Eino Study Notes — Appendix 2
Eino Study Notes — Appendix 2: The Components Module
This note was generated with AI assistance, but the code-reading order should be fine (all examples use the official Eino examples; personally, I find the official tutorials a bit hard to follow).
In principle, I think the appendices are just for reference — you can skim them and look things up when needed.
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 components implement a unified interface and can be freely plugged in/out within Graph/Agent/Runner.
At a Glance: Component System Overview
Eino’s component layer is the building block for all higher-level abstractions (ADK, Flow, Compose). Each component type defines a standard interface, and different implementations can be swapped seamlessly:
┌─────────────────────────────────────────┐│ ADK / Flow / Compose │ ← Upper-layer orchestration├─────────────────────────────────────────┤│ ChatModel │ Retriever │ Parser │ ... │ ← Component interfaces├─────────────────────────────────────────┤│ OpenAI │ ARK │ VikingDB │ PDF │ HTML │ ← Concrete implementations└─────────────────────────────────────────┘📇 Quick-Reference Card for This Appendix
Three major component categories: Model: A/B routing (ABRouter) / HTTP logging (CurlRT) Retriever: Multi-Query (rewrite query) / Router (multi-data-source routing) Parser: TextParser → ExtParser → CustomParser (dispatch by extension)
Core pattern: All implement a unified interface → freely pluggable in any orchestration1. Model: A/B Test Routing
📁
components/model/abtest/
1.1 What problem does this solve?
Those with a data-analysis background will surely be familiar with this.
Suppose you wrote an Agent using GPT-4o. Now the team has onboarded DeepSeek, which is 80% cheaper but possibly slightly lower quality. Would you dare to fully switch over to it right away?
No. What you’d prefer is:
100% of user requests ├─ 90% → GPT-4o (main, stable) └─ 10% → DeepSeek (experiment, observe)Run it for a week and compare user satisfaction, latency, and cost between the two groups — let the data decide whether to switch. That is A/B testing.
ABRouterChatModel is exactly this “traffic distributor” — externally identical to a normal ChatModel, but internally it routes 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 to DeepSeek Model: "deepseek-chat",})
// Router: A/B split based on the last digit of userIDrouter := abtest.NewABRouterChatModel(func( ctx context.Context, in []*schema.Message, _ ...model.Option,) (string, model.BaseChatModel, error) { // Extract userID from 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 the same as a normal ChatModel!msg, _ := router.Generate(ctx, messages)1.3 More Than A/B Testing — Three Real Use Cases
| Scenario | Routing rule | Purpose |
|---|---|---|
| A/B testing | userID hash → 10% new model, 90% old model | Compare results before deciding on full switch |
| Cost optimization | Message length < 100 → cheap model; ≥ 100 → strong model | Simple questions don’t need a sledgehammer |
| Canary release | 1% traffic → new model; observe one day with no anomalies → 5% → 20% → 100% | Gradually ramp up; quick rollback if issues arise |
🧠 Key understanding: The key value of ABRouter is that the external interface stays unchanged — your Agent code doesn’t need to know how many models are running underneath, and changing routing rules requires no changes to business code.
2. Model: HTTP Transport Logging (cURL-style Debugging)
📁
components/model/httptransport/
2.1 What problem does this solve?
You wrote an Agent, and after running it the LLM returned a strange result. You want to confirm:
- What did the request actually sent to the API look like? (Was the prompt truncated? Are the parameters correct?)
- What was the raw response returned by the API? (Did JSON parsing go wrong?)
- Can you copy the request as a cURL command and replay it in the terminal?
httptransport does exactly this — it intercepts all HTTP requests/responses and prints logs in a copy-pasteable cURL command format, while automatically redacting the API Key.
2.2 Core Mechanism
client := &http.Client{ Transport: httptransport.NewCurlRT( http.DefaultTransport, // Base Transport (the one that actually sends requests) 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 ChatModel — logs are printed automatically on subsequent callschatModel, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{ HTTPClient: client,})2.3 Log Output Effect
Request log — copy directly to the terminal to 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 returned by the API:
[curl response] HTTP/1.1 200{"id":"chatcmpl-xxx","choices":[{"message":{"role":"assistant","content":"Hello! How can I help?"}}]}2.4 Configuration Quick Reference
| Option | Effect | Default |
|---|---|---|
WithPrintAuth(false) | Don’t print Authorization Header | Redacted |
WithMaskHeaders(...) | Extra headers to redact | — |
WithStreamLogging(true) | Log streaming responses chunk by chunk | Not logged |
WithMaxStreamLogBytes(n) | Max bytes for streaming log | 8192 |
WithCtxLogger(...) | Extract request ID from ctx and inject into log | — |
🧠 Key understanding: This is a debugging/troubleshooting tool, not a production necessity. Recommended to keep it on during development, and turn it off in production or only enable it 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 to improve code quality"The actual titles of relevant docs in the vector store are: "Unit Testing Best Practices" "Code Review Process Guidelines" "Static Analysis Tool Configuration Guide"Searching directly with “How to improve code quality” in the vector store may return nothing — there is a semantic gap between the user’s colloquial phrasing and the document’s technical terminology.
Multi-Query’s approach: First let the LLM translate the user’s words into 3 queries from different angles, search each one, then merge and deduplicate the results.
3.2 Workflow
User input: "tourist attraction" (too vague) ↓ LLM auto-rewritesVariant 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 attractions" angle ↓ Retrieve from the same vector store separatelyResult 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]3.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 contains the merged results from all variants🧠 Key understanding: Multi-Query is “using an LLM for query augmentation” — use a cheap, fast model to do the rewriting, then take the rewritten results to search the vector store. FusionFunc dedupes by document ID by default, preserving the order of first appearance.
4. Retriever: Router Retrieval
📁
components/retriever/router/
4.1 What problem does this solve?
Multi-Query solves the problem of “searching one knowledge base inaccurately.” Router solves the problem of “multiple knowledge bases, not knowing which one to search.”
Your system has three vector stores: product_vdb → stores product info (name, price, specs) review_vdb → stores user reviews (ratings, review text) faq_vdb → stores FAQs (Q&A pairs)
User asks "How much does this product cost?" → search product_vdbUser asks "Is this product good?" → search review_vdbUser asks "How to return it?" → search faq_vdbRouter Retriever lets you declare “which stores exist + which queries go to which store,” and a single call automatically routes and merges.
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 | User’s phrasing too vague, vector store search inaccurate | Multiple vector stores, not knowing which to search |
| Retrieval count | N variants × 1 retriever = N times | M selected retrievers × 1 time = M times |
| Core idea | Use LLM to rewrite query, search from multiple angles | Analyze query content, dispatch to corresponding stores |
| Use case | Single knowledge base, improve recall | Multiple knowledge bases, joint retrieval |
💡 The two can be combined — first Router to multiple retrievers, then Multi-Query rewrites for each retriever, then global fusion.
5. Document Parser
5.1 Text Parser (Simplest Entry Point)
import "github.com/cloudwego/eino/components/document/parser"
textParser := parser.TextParser{}docs, _ := textParser.Parse(ctx, strings.NewReader("hello world"))5.2 ExtParser: Auto-Select Parser by File Extension
Motivation: The file you upload might be HTML, PDF, Markdown, or plain text. Writing a bunch of if/switch is too tedious. ExtParser automatically dispatches to the corresponding Parser based on the file suffix:
extParser, _ := parser.NewExtParser(ctx, &parser.ExtParserConfig{ Parsers: map[string]parser.Parser{ ".html": htmlParser, // → Use CSS Selector to extract body content ".pdf": pdfParser, // → Use PDF parsing engine to extract text }, 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 determines the format by the file path suffix5.3 Custom Parser (Four-Step Implementation)
When the built-in TextParser / HTML / PDF don’t meet your needs (e.g., parsing Word documents, Excel tables, or custom formats):
- Define the custom options struct (
type options struct{...}) - Create 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()extracts framework-common options (WithURI, WithExtraMeta)parser.GetImplSpecificOptions()extracts custom optionsparser.WrapImplSpecificOptFnwraps a customfunc(*options)into aparser.Option
6. Panoramic Quick Reference
| Component type | What it solves | Section | Core API |
|---|---|---|---|
| ABRouter | Dare not fully switch to new model → split traffic by rules for A/B comparison | 1 | NewABRouterChatModel(routerFn) |
| CurlRT | API crashed and you don’t know what was sent → cURL-style log debugging | 2 | NewCurlRT(transport, opts...) |
| Multi-Query | User searches too vaguely → LLM rewrites N variants and searches each | 3 | multiquery.NewRetriever({RewriteHandler, OrigRetriever}) |
| Router Retriever | Multiple vector stores, don’t know which to search → auto-route by query content | 4 | router.NewRetriever({Retrievers, Router}) |
| ExtParser | Multiple 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 parser"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 │└──────────────────────────────────────────────────┘📇 Quick-Reference Card for This Appendix
Model: ABRouter = traffic distributor (A/B testing / cost optimization / canary release) CurlRT = HTTP debugger (cURL-style logs + automatic API Key redaction)
Retriever: MultiQuery = use LLM to rewrite query → search same store from multiple angles → merge and dedupe Router = analyze query content → dispatch to different stores → fuse 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 writing / Devops / Mermaid visualization