Eino Study Notes — Appendix 2
Eino Study Notes — Appendix 2: the Components module
This note was written with AI assistance, but the code reading order should be fine (everything follows the official Eino examples; I personally find the official tutorials a little hard to follow).
In principle I think the appendices only need a skim — look things up when you need them.
Appendix 2: The Components Module
📂 Source repo: github.com/cloudwego/eino-examples
📖 Series docs: Intro Notes | Agentic Advanced | Appendix 1 Flow | Appendix 2 Components | Appendix 3 Tools
📁 Code root:
components/📖 Prerequisites: this appendix focuses on Eino’s replaceable components — Model, Retriever, and Document Parser. These implement a unified interface and can be plugged in and out of Graph/Agent/Runner freely.
At a Glance: Component System Overview
Eino’s component layer is the building block for every higher-level abstraction (ADK, Flow, Compose).
Each component type defines one standard interface, and different implementations can be swapped in seamlessly:
┌─────────────────────────────────────────┐│ ADK / Flow / Compose │ ← upper-layer orchestration├─────────────────────────────────────────┤│ ChatModel │ Retriever │ Parser │ ... │ ← component interfaces├─────────────────────────────────────────┤│ OpenAI │ ARK │ VikingDB │ PDF │ HTML │ ← concrete implementations└─────────────────────────────────────────┘📇 Cheat Sheet for This Appendix
Three component categories: Model: A/B routing (ABRouter) / HTTP logging (CurlRT) Retriever: Multi-Query (rewrite the query) / Router (route across data sources) Parser: TextParser → ExtParser → CustomParser (dispatch by extension)
Core pattern: all implement one interface → pluggable in any orchestration1. Model: A/B Test Routing
📁
components/model/abtest/
1.1 What Problem Does This Solve?
Anyone with a bit of analytics background will recognize this one.
Say you wrote an Agent on GPT-4o.
Now the team has brought in DeepSeek — 80% cheaper, but possibly a little worse in quality.
Do you dare cut all the traffic over to it?
No.
What you actually want is:
100% of user requests ├─ 90% → GPT-4o (workhorse, stable) └─ 10% → DeepSeek (experiment, observe)Run it for a week, then compare satisfaction, latency, and cost across the two groups.
Let the data speak, then decide whether to switch.
That is A/B testing.
ABRouterChatModel is exactly this “traffic splitter”.
Externally it is identical to an ordinary ChatModel, but internally it hands requests to different models according to the rules you write.
1.2 Full Example
import "github.com/cloudwego/eino-examples/components/model/abtest"
// Prepare two candidate modelsgpt4o, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{...})deepseek, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{ BaseURL: "https://api.deepseek.com/v1", // point at DeepSeek Model: "deepseek-chat",})
// Router: split A/B on the last digit of userIDrouter := abtest.NewABRouterChatModel(func( ctx context.Context, in []*schema.Message, _ ...model.Option,) (string, model.BaseChatModel, error) { // Pull userID out of the context (in a real project, from the request context) userID, _ := ctx.Value("userID").(string) if userID != "" && userID[len(userID)-1] < '5' { // 50% of traffic return "deepseek", deepseek, nil // experiment group } return "gpt4o", gpt4o, nil // control group})
// Used exactly like an ordinary ChatModel!msg, _ := router.Generate(ctx, messages)1.3 Beyond A/B Testing — Three Real Use Cases
| Scenario | Routing rule | Purpose |
|---|---|---|
| A/B testing | userID hash → 10% new model, 90% old model | Compare the results, then decide on a full switch |
| Cost optimization | message length < 100 → cheap model; ≥ 100 → strong model | Simple questions don’t need a sledgehammer |
| Canary release | 1% of traffic → new model; a day with no anomalies → 5% → 20% → 100% | Ramp up gradually, roll back fast when it breaks |
🧠 Key insight: ABRouter’s real value is that the external interface does not change. Your Agent code never needs to know how many models run underneath, and changing routing rules never requires touching business code.
2. Model: HTTP Transport Logging
📁
components/model/httptransport/
2.1 What Problem Does This Solve?
You wrote an Agent, ran it, and the LLM came back with something strange.
You want to confirm:
- What actually went to the API — what did the request look like? (Was the prompt truncated? Are the parameters right?)
- What the API actually returned — what is the raw response? (Did JSON parsing go wrong?)
- Can you copy the request into a cURL command and replay it in a terminal?
httptransport does exactly this.
It intercepts every HTTP request and response, prints the log in a directly copyable cURL command format, and redacts the API Key automatically.
2.2 Core Mechanism
client := &http.Client{ Transport: httptransport.NewCurlRT( http.DefaultTransport, // base Transport (the one that actually sends) httptransport.WithLogger(log.Default()), httptransport.WithPrintAuth(false), // ⚠️ API Key redacted (not printed by default) httptransport.WithMaskHeaders([]string{"X-API-KEY"}), httptransport.WithStreamLogging(true), // log streaming responses too httptransport.WithMaxStreamLogBytes(8192), ),}
// Inject into the ChatModel — logs print automatically on later callschatModel, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{ HTTPClient: client,})2.3 What the Logs Look Like
Request log — copy it straight into a terminal and replay:
[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"}]}'Response log — see the raw JSON the API returned:
[curl response] HTTP/1.1 200{"id":"chatcmpl-xxx","choices":[{"message":{"role":"assistant","content":"Hello! How can I help?"}}]}2.4 Config Quick Reference
| Option | Effect | Default |
|---|---|---|
WithPrintAuth(false) | Don’t print the Authorization header | Redacted |
WithMaskHeaders(...) | Extra headers to redact | — |
WithStreamLogging(true) | Log streaming responses chunk by chunk | Not logged |
WithMaxStreamLogBytes(n) | Max bytes of the streaming log | 8192 |
WithCtxLogger(...) | Pull a request ID out of ctx into the log | — |
🧠 Key insight: this is a debugging and troubleshooting tool, not something production has to run. Keep it on while developing; in production turn it off, or enable it only on sampled traffic.
3. Retriever: Multi-Query Retrieval
📁
components/retriever/multiquery/
3.1 What Problem Does This Solve?
In RAG (retrieval-augmented generation) scenarios the user’s query is often not precise enough:
User input: "how do I improve code quality"The titles of the relevant docs in the vector store are actually: "Unit Testing Best Practices" "Code Review Process Guidelines" "Static Analysis Tool Configuration Guide"Search the vector store directly with “how do I improve code quality” and you may get nothing back.
Between the user’s colloquial phrasing and the document’s technical terms there is a semantic gap.
Multi-Query’s approach: first have the LLM translate what the user said into 3 queries from different angles, search once with each, then merge and deduplicate the results.
3.2 Workflow
User input: "tourist attraction" (too vague) ↓ LLM rewrites automaticallyVariant 1: "best tourist attractions for families" ← from the "family" angleVariant 2: "outdoor sightseeing spots recommendations" ← from the "outdoor" angleVariant 3: "popular travel destinations and landmarks" ← from the "famous sights" angle ↓ search the same vector store with eachResult 1: [doc_a, doc_b]Result 2: [doc_b, doc_d] → FusionFunc merges and dedupes → [doc_a, doc_b, doc_c, doc_d, doc_e, doc_f]Result 3: [doc_c, doc_e, doc_f]The same flow as a sequence diagram:
sequenceDiagram
actor U as User code
participant M as MultiQuery Retriever
participant L as LLM (rewrite)
participant V as Vector store
participant F as FusionFunc
U->>M: Retrieve(ctx, "tourist attraction")
M->>L: rewrite the original query
L-->>M: variant 1 / variant 2 / variant 3
M->>V: search each variant separately
V-->>M: result 1 / result 2 / result 3
M->>F: merge the three result sets
F-->>U: deduplicated document list3.3 Full Implementation
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, // generate at most 3 variants OrigRetriever: vk, // underlying vector retrieval engine})
docs, _ := mqr.Retrieve(ctx, "tourist attraction")// docs holds the merged result of all variants🧠 Key insight: Multi-Query is “query augmentation with an LLM”. Use a cheap, fast model to rewrite, then take the rewritten queries to the vector store. FusionFunc dedupes by document ID by default and keeps the order of first appearance.
4. Retriever: Router Retrieval
📁
components/retriever/router/
4.1 What Problem Does This Solve?
Multi-Query solves “searching inaccurately within one knowledge base”.
Router solves “several knowledge bases, and no idea which one to search”.
Your system has three vector stores: product_vdb → product info (name, price, specs) review_vdb → user reviews (rating, review text) faq_vdb → FAQs (Q&A pairs)
User asks "how much is this product?" → search product_vdbUser asks "is this product any good?" → search review_vdbUser asks "how do I return it?" → search faq_vdbRouter Retriever is declaring “which stores exist + which queries go to which store”; a single call routes and merges automatically.
4.2 Full Implementation
rr, _ := router.NewRetriever(ctx, &router.Config{ Retrievers: map[string]retriever.Retriever{ "product": productRetriever, // product vector store "review": reviewRetriever, // review vector store "faq": faqRetriever, // FAQ vector store }, Router: func(ctx context.Context, query string) ([]string, error) { var targets []string // Route to different engines by keyword if strings.Contains(query, "price") || strings.Contains(query, "specs") { targets = append(targets, "product") } if strings.Contains(query, "review") || strings.Contains(query, "good") { targets = append(targets, "review") } if strings.Contains(query, "return") || strings.Contains(query, "how") { targets = append(targets, "faq") } if len(targets) == 0 { targets = []string{"product", "review", "faq"} // fallback: search all } return targets, nil }, FusionFunc: nil, // nil = default RRF (Reciprocal Rank Fusion) fusion})4.3 Difference from Multi-Query
| Dimension | Multi-Query | Router |
|---|---|---|
| What it solves | The user’s phrasing is too vague, the store can’t find it | Several vector stores, no idea which one to search |
| Retrieval count | N variants × 1 retriever = N calls | M selected retrievers × 1 call = M calls |
| Core idea | Rewrite the query with an LLM, search from several angles | Analyze the query, dispatch to the matching store |
| Fits | A single knowledge base, raising recall | Several knowledge bases, joint retrieval |
💡 The two can be combined. Route to several retrievers first, have each retriever do Multi-Query rewriting, then fuse globally.
5. Document Parser
5.1 Text Parser (Simplest Entry)
import "github.com/cloudwego/eino/components/document/parser"
textParser := parser.TextParser{}docs, _ := textParser.Parse(ctx, strings.NewReader("hello world"))5.2 ExtParser
Motivation: the file you upload might be HTML, PDF, Markdown, or plain text.
Writing a pile of if/switch is too much bother.
ExtParser dispatches to the matching Parser by file extension:
extParser, _ := parser.NewExtParser(ctx, &parser.ExtParserConfig{ Parsers: map[string]parser.Parser{ ".html": htmlParser, // → extract body content with a CSS selector ".pdf": pdfParser, // → extract text with a PDF parsing engine }, FallbackParser: textParser, // unknown format → read as plain text})
file, _ := os.Open("./testdata/test.html")docs, _ := extParser.Parse(ctx, file, parser.WithURI(filePath))// ↑ WithURI must be passed — ExtParser reads the extension off the file path5.3 Custom Parser
When the built-in TextParser / HTML / PDF doesn’t fit the need (parsing Word documents, Excel sheets, a custom format):
- Define the custom options struct (
type options struct{...}) - Build the Option function with
parser.WrapImplSpecificOptFn - Define the Config + struct + constructor
- Implement
Parse(ctx, io.Reader, ...parser.Option) ([]*schema.Document, error)
Core techniques:
parser.GetCommonOptions()pulls out the framework-common options (WithURI, WithExtraMeta)parser.GetImplSpecificOptions()pulls out the custom optionsparser.WrapImplSpecificOptFnwraps a customfunc(*options)into aparser.Option
6. Panoramic Quick Reference
| Component type | What it solves | Section | Core API |
|---|---|---|---|
| ABRouter | Afraid to switch to the new model wholesale → split traffic by rule for an A/B comparison | 1 | NewABRouterChatModel(routerFn) |
| CurlRT | The API broke and you don’t know what was sent → cURL-style log debugging | 2 | NewCurlRT(transport, opts...) |
| Multi-Query | The user’s search is too vague → the LLM rewrites N variants and searches each | 3 | multiquery.NewRetriever({RewriteHandler, OrigRetriever}) |
| Router Retriever | Several vector stores, no idea which to search → auto-route by query content | 4 | router.NewRetriever({Retrievers, Router}) |
| ExtParser | Many document formats → auto-dispatch by extension | 5 | NewExtParser({Parsers: map[".ext"]parser}) |
Common Import Paths
// A/B test routing"github.com/cloudwego/eino-examples/components/model/abtest"
// HTTP transport logging"github.com/cloudwego/eino-examples/components/model/httptransport"
// Retriever augmentation"github.com/cloudwego/eino/flow/retriever/multiquery" // Multi-Query"github.com/cloudwego/eino/flow/retriever/router" // Router
// Document parsers"github.com/cloudwego/eino/components/document/parser" // Parser interface, TextParser, ExtParser"github.com/cloudwego/eino-ext/components/document/parser/html" // HTML"github.com/cloudwego/eino-ext/components/document/parser/pdf" // PDFLayered Architecture Review
┌──────────────────────────────────────────────────┐│ ADK layer (Intro Notes §1~§7) ││ ChatModelAgent, Runner, Middleware, Interrupt │├──────────────────────────────────────────────────┤│ Flow layer (Appendix 1: Flow module) ││ react.NewAgent, host.NewMultiAgent │├──────────────────────────────────────────────────┤│ Compose layer (Intro Notes §6.4) ││ Graph, Chain, Workflow, Branch, ProcessState │├──────────────────────────────────────────────────┤│ Component layer (this appendix) ││ ChatModel, Retriever, Parser, Tool │└──────────────────────────────────────────────────┘📇 Cheat Sheet for This Appendix
Model: ABRouter = traffic splitter (A/B testing / cost optimization / canary release) CurlRT = HTTP debugger (cURL-style logs + automatic API Key redaction)
Retriever: MultiQuery = rewrite the query with an LLM → search one store from several angles → merge and dedupe Router = analyze the query → dispatch to different stores → fuse the results
Parser: TextParser → ExtParser (map by extension) → CustomParser (WrapImplSpecificOptFn)📂 Source repo: github.com/cloudwego/eino-examples
📖 Keep reading:
- Intro Notes — Eino ADK from Hello World to Compose orchestration
- Agentic Advanced — Responses API / AgenticMessage / Typed generics
- Appendix 1: Flow Module — ReAct Agent / Multi-Agent / state graph
- Appendix 2: Components Module — A/B routing / HTTP logging / retrieval augmentation / document parsing ← you are here
- Appendix 3: Lambda and Debugging Tools — Lambda patterns / Devops / Mermaid visualization