Eino学习笔记 —— 附录2 - MuxiaoWF跳到主要内容

Eino学习笔记 —— 附录2

Eino学习笔记 —— 附录二:Components 组件模块

周一 7月 20 2026
2903 字 · 15 分钟

本笔记通过ai辅助生成,但对于代码阅读顺序应当没有问题(均使用eino官方的例子进行,个人认为官方的教程感觉有点太难懂了)

理论上个人感觉附录只做了解即可,需要时可进行查询

附录二:Components 组件模块

📂 源码仓库github.com/cloudwego/eino-examples

📖 系列文档入门笔记 | Agentic 进阶 | 附录一 Flow | 附录二 组件 | 附录三 工具

📁 代码根目录:components/

📖 前置知识:本附录聚焦 Eino 的可替换组件——Model(模型)、Retriever(检索器)、Document Parser(文档解析器)。这些组件实现统一接口,可在 Graph/Agent/Runner 中自由插拔。


速览:组件体系概述

Eino 的组件层是所有上层抽象(ADK、Flow、Compose)的积木块。每个组件类型定义了一个标准接口,不同实现可以无缝替换:

┌─────────────────────────────────────────┐
│ ADK / Flow / Compose │ ← 上层编排
├─────────────────────────────────────────┤
│ ChatModel │ Retriever │ Parser │ ... │ ← 组件接口
├─────────────────────────────────────────┤
│ OpenAI │ ARK │ VikingDB │ PDF │ HTML │ ← 具体实现
└─────────────────────────────────────────┘

📇 本附录速记卡片

组件层三大类:
Model: A/B 路由(ABRouter)/ HTTP 日志(CurlRT)
Retriever: Multi-Query(改写查询)/ Router(多数据源路由)
Parser: TextParser → ExtParser → CustomParser(按扩展名分发)
核心模式:全都实现统一接口 → 可在任何编排中自由插拔

一、Model:A/B 测试路由

📁 components/model/abtest/

1.1 这东西解决什么问题?

相信有数分基础的对这个肯定很熟悉

假设你写了一个 Agent,用的是 GPT-4o。现在团队新接入了 DeepSeek,便宜 80% 但质量可能稍差。你敢直接全量切过去吗?

不敢。 你希望的是:

100% 用户请求
├─ 90% → GPT-4o(主力,稳定)
└─ 10% → DeepSeek(实验,观察)

跑一周后对比两组用户的满意度、延迟、成本——数据说话,决定切不切。这就是 A/B 测试

ABRouterChatModel 就是这个”流量分配器”——对外和普通 ChatModel 一模一样,但内部根据你写的规则把请求分给不同模型。

1.2 完整示例

import "github.com/cloudwego/eino-examples/components/model/abtest"
// 准备两个候选模型
gpt4o, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{...})
deepseek, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: "https://api.deepseek.com/v1", // 指向 DeepSeek
Model: "deepseek-chat",
})
// 路由器:根据 userID 最后一位做 A/B 分流
router := abtest.NewABRouterChatModel(func(
ctx context.Context, in []*schema.Message, _ ...model.Option,
) (string, model.BaseChatModel, error) {
// 从 context 中提取 userID(实际项目中从请求上下文拿)
userID, _ := ctx.Value("userID").(string)
if userID != "" && userID[len(userID)-1] < '5' { // 50% 流量
return "deepseek", deepseek, nil // 实验组
}
return "gpt4o", gpt4o, nil // 对照组
})
// 使用方式和普通 ChatModel 完全一样!
msg, _ := router.Generate(ctx, messages)

1.3 不止 A/B 测试——三种实际应用场景

场景路由规则目的
A/B 测试userID hash → 10% 新模型,90% 旧模型对比效果后决定是否全量切换
成本优化消息长度 < 100 → 便宜模型;≥ 100 → 强模型简单问题不需要大炮打蚊子
灰度发布1% 流量 → 新模型;观测一天无异常 → 5% → 20% → 100%逐步放量,出问题快速回滚

🧠 关键理解:ABRouter 的关键价值是对外接口不变——你的 Agent 代码不需要知道底层有几个模型在跑,改路由规则不需要改业务代码。


二、Model:HTTP 传输日志(cURL 风格调试)

📁 components/model/httptransport/

2.1 这东西解决什么问题?

你写了一个 Agent,跑起来后 LLM 返回了奇怪的结果。你想确认:

  • 真正发给 API 的请求长什么样?(prompt 有没有被截断?参数对不对?)
  • API 返回的原始响应是什么?(是不是 JSON 解析出了问题?)
  • 能不能把请求复制成 cURL 命令,在终端里重放?

httptransport 就是做这个的——它拦截所有 HTTP 请求/响应,以可直接复制的 cURL 命令格式打印日志,同时自动脱敏 API Key。

2.2 核心机制

client := &http.Client{
Transport: httptransport.NewCurlRT(
http.DefaultTransport, // 基础 Transport(真正发请求的)
httptransport.WithLogger(log.Default()),
httptransport.WithPrintAuth(false), // ⚠️ API Key 脱敏(默认不打印)
httptransport.WithMaskHeaders([]string{"X-API-KEY"}),
httptransport.WithStreamLogging(true), // 流式响应也记录
httptransport.WithMaxStreamLogBytes(8192),
),
}
// 注入到 ChatModel — 之后调用时自动打印日志
chatModel, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
HTTPClient: client,
})

2.3 日志输出效果

请求日志——直接复制到终端就能重放:

[curl request] curl -X POST 'https://api.openai.com/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: <redacted>' \
--data '{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}'

响应日志——看到 API 返回的原始 JSON:

[curl response] HTTP/1.1 200
{"id":"chatcmpl-xxx","choices":[{"message":{"role":"assistant","content":"Hello! How can I help?"}}]}

2.4 配置速查

选项作用默认值
WithPrintAuth(false)不打印 Authorization Header脱敏
WithMaskHeaders(...)额外脱敏的 Header
WithStreamLogging(true)流式响应逐 chunk 记录不记录
WithMaxStreamLogBytes(n)流式日志最大字节数8192
WithCtxLogger(...)从 ctx 提取 request ID 注入日志

🧠 关键理解:这是一个调试/排障工具,不是生产必用的。建议开发时开着,生产环境关闭或只在采样流量中开启。


三、Retriever:多查询检索(Multi-Query)

📁 components/retriever/multiquery/

3.1 这东西解决什么问题?

在 RAG(检索增强生成)场景中,用户的查询往往不够精确

用户输入: "怎么提高代码质量"
向量库里相关文档的标题实际是:
"单元测试最佳实践"
"Code Review 流程规范"
"静态分析工具配置指南"

直接拿”怎么提高代码质量”去向量检索,可能什么都搜不到——用户的口语和文档的专业术语之间有语义鸿沟

Multi-Query 的解决思路:先让 LLM 把用户的话翻译成 3 个不同角度的查询,每个都去搜一遍,结果合并去重。

3.2 工作流程

用户输入: "tourist attraction"(太模糊)
↓ LLM 自动改写
变体1: "best tourist attractions for families" ← 从"亲子"角度
变体2: "outdoor sightseeing spots recommendations" ← 从"户外"角度
变体3: "popular travel destinations and landmarks" ← 从"知名景点"角度
↓ 分别检索同一向量库
结果1: [doc_a, doc_b]
结果2: [doc_b, doc_d] → FusionFunc 合并去重 → [doc_a, doc_b, doc_c, doc_d, doc_e, doc_f]
结果3: [doc_c, doc_e, doc_f]

3.3 完整实现

import "github.com/cloudwego/eino/flow/retriever/multiquery"
mqr, _ := multiquery.NewRetriever(ctx, &multiquery.Config{
RewriteHandler: func(ctx context.Context, query string) ([]string, error) {
out, _ := llm.Generate(ctx, []*schema.Message{
schema.SystemMessage("Generate 3 rewritten search queries, one per line."),
schema.UserMessage(query),
})
return strings.Split(out.Content, "\n"), nil
},
MaxQueriesNum: 3, // 最多生成 3 个变体
OrigRetriever: vk, // 底层向量检索引擎
})
docs, _ := mqr.Retrieve(ctx, "tourist attraction")
// docs 包含所有变体的合并结果

🧠 关键理解:Multi-Query 是”用 LLM 做查询增强”——用便宜的、快速的模型做改写,拿改写结果去向量库搜。FusionFunc 默认按文档 ID 去重,保留第一次出现的顺序。


四、Retriever:路由检索(Router)

📁 components/retriever/router/

4.1 这东西解决什么问题?

Multi-Query 解决的是”一个知识库里搜不准”的问题。Router 解决的是”多个知识库,不知道该搜哪个”的问题。

你的系统有三个向量库:
product_vdb → 存商品信息(名称、价格、规格)
review_vdb → 存用户评价(打分、评论文本)
faq_vdb → 存常见问题(Q&A 对)
用户问"这个商品多少钱?" → 搜 product_vdb
用户问"这个商品好用吗?" → 搜 review_vdb
用户问"怎么退货?" → 搜 faq_vdb

Router Retriever 就是声明”有哪些库 + 什么样的查询走哪个库”,一次调用自动路由+合并。

4.2 完整实现

rr, _ := router.NewRetriever(ctx, &router.Config{
Retrievers: map[string]retriever.Retriever{
"product": productRetriever, // 商品向量库
"review": reviewRetriever, // 评价向量库
"faq": faqRetriever, // FAQ 向量库
},
Router: func(ctx context.Context, query string) ([]string, error) {
var targets []string
// 按关键词路由到不同引擎
if strings.Contains(query, "价格") || strings.Contains(query, "规格") {
targets = append(targets, "product")
}
if strings.Contains(query, "评价") || strings.Contains(query, "好用") {
targets = append(targets, "review")
}
if strings.Contains(query, "退货") || strings.Contains(query, "怎么") {
targets = append(targets, "faq")
}
if len(targets) == 0 {
targets = []string{"product", "review", "faq"} // 兜底:全搜
}
return targets, nil
},
FusionFunc: nil, // nil = 默认 RRF(Reciprocal Rank Fusion)融合
})

4.3 与 Multi-Query 的区别

维度Multi-QueryRouter
解决什么问题用户说的话太模糊,向量库搜不准有多个向量库,不知道该搜哪个
检索次数N 个变体 ×1 个检索器 = N 次M 个选中检索器 × 1 次 = M 次
核心思路用 LLM 改写查询,多角度搜分析查询内容,分派到对应库
适用单一知识库,提升召回率多知识库,联合检索

💡 两者可以组合——先 Router 到多个检索器,每个检索器再 Multi-Query 改写,然后全局融合。


五、Document Parser

5.1 文本解析器(最简入口)

import "github.com/cloudwego/eino/components/document/parser"
textParser := parser.TextParser{}
docs, _ := textParser.Parse(ctx, strings.NewReader("hello world"))

5.2 ExtParser:按文件扩展名自动选择解析器

动机:你上传的文件可能是 HTML、PDF、Markdown 或纯文本。写一堆 if/switch 太麻烦。ExtParser 根据文件后缀自动分发给对应的 Parser:

extParser, _ := parser.NewExtParser(ctx, &parser.ExtParserConfig{
Parsers: map[string]parser.Parser{
".html": htmlParser, // → 用 CSS Selector 提取 body 内容
".pdf": pdfParser, // → 用 PDF 解析引擎提取文本
},
FallbackParser: textParser, // 未知格式 → 当纯文本读
})
file, _ := os.Open("./testdata/test.html")
docs, _ := extParser.Parse(ctx, file, parser.WithURI(filePath))
// ↑ WithURI 必须传——ExtParser 靠文件路径后缀判断格式

5.3 自定义 Parser(四步实现)

当内置的 TextParser / HTML / PDF 不满足需求时(比如解析 Word 文档、Excel 表格、自定义格式):

  1. 定义自定义选项结构体 (type options struct{...})
  2. parser.WrapImplSpecificOptFn 创建 Option 函数
  3. 定义 Config + 结构体 + 构造函数
  4. 实现 Parse(ctx, io.Reader, ...parser.Option) ([]*schema.Document, error)

核心技巧:

  • parser.GetCommonOptions() 提取框架通用选项(WithURI, WithExtraMeta)
  • parser.GetImplSpecificOptions() 提取自定义选项
  • parser.WrapImplSpecificOptFn 将自定义 func(*options) 包装成 parser.Option

六、全景速查

组件类型解决什么问题章节核心 API
ABRouter新模型不敢全量切 → 按规则分流量 A/B 对比NewABRouterChatModel(routerFn)
CurlRTAPI 调崩了不知道发了什么 → cURL 风格日志调试NewCurlRT(transport, opts...)
Multi-Query用户搜得太模糊 → LLM 改写 N 个变体分别搜multiquery.NewRetriever({RewriteHandler, OrigRetriever})
Router Retriever多个向量库不知搜哪个 → 按查询内容自动路由router.NewRetriever({Retrievers, Router})
ExtParser多种文档格式 → 按扩展名自动分发NewExtParser({Parsers: map[".ext"]parser})

常用 import 路径

// A/B 测试路由
"github.com/cloudwego/eino-examples/components/model/abtest"
// HTTP 传输日志
"github.com/cloudwego/eino-examples/components/model/httptransport"
// 检索器增强
"github.com/cloudwego/eino/flow/retriever/multiquery" // Multi-Query
"github.com/cloudwego/eino/flow/retriever/router" // Router
// 文档解析器
"github.com/cloudwego/eino/components/document/parser" // Parser 接口, TextParser, ExtParser
"github.com/cloudwego/eino-ext/components/document/parser/html" // HTML
"github.com/cloudwego/eino-ext/components/document/parser/pdf" // PDF

分层架构回顾

┌──────────────────────────────────────────────────┐
│ ADK 层(入门笔记 §1~§7) │
│ ChatModelAgent, Runner, Middleware, Interrupt │
├──────────────────────────────────────────────────┤
│ Flow 层(附录一:Flow 流程模块) │
│ react.NewAgent, host.NewMultiAgent │
├──────────────────────────────────────────────────┤
│ Compose 层(入门笔记 §6.4) │
│ Graph, Chain, Workflow, Branch, ProcessState │
├──────────────────────────────────────────────────┤
│ 组件层(本附录) │
│ ChatModel, Retriever, Parser, Tool │
└──────────────────────────────────────────────────┘

📇 本附录速记卡片

Model:
ABRouter = 流量分配器(A/B测试 / 成本优化 / 灰度发布)
CurlRT = HTTP 调试器(cURL风格日志 + API Key 自动脱敏)
Retriever:
MultiQuery = 用 LLM 改写查询 → 多角度搜同一个库 → 合并去重
Router = 分析查询内容 → 分派到不同库 → 结果融合
Parser:
TextParser → ExtParser(按扩展名映射) → CustomParser(WrapImplSpecificOptFn)

📂 源码仓库github.com/cloudwego/eino-examples

📖 继续阅读


感谢您的阅读!如果可以,给俺点些关注吧~

Eino学习笔记 —— 附录2

周一 7月 20 2026
2903 · 15 分钟
封面
示例歌曲
示例艺术家
封面
示例歌曲
示例艺术家
0:00 / 0:00