Eino学习笔记 —— 附录2
Eino学习笔记 —— 附录二:Components 组件模块
本笔记通过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_vdbRouter 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-Query | Router |
|---|---|---|
| 解决什么问题 | 用户说的话太模糊,向量库搜不准 | 有多个向量库,不知道该搜哪个 |
| 检索次数 | 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 表格、自定义格式):
- 定义自定义选项结构体 (
type options struct{...}) - 用
parser.WrapImplSpecificOptFn创建 Option 函数 - 定义 Config + 结构体 + 构造函数
- 实现
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) |
| CurlRT | API 调崩了不知道发了什么 → 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 ADK 从 Hello World 到 Compose 编排
- Agentic 进阶 — Responses API / AgenticMessage / Typed 泛型
- 附录一:Flow 流程模块 — ReAct Agent / Multi-Agent / 状态图
- 附录二:Components 组件模块 — A/B 路由 / HTTP 日志 / 检索增强 / 文档解析 ← 你在这里
- 附录三:Lambda 与调试工具 — Lambda 写法 / Devops / Mermaid 可视化