Skip to main content
This page was machine-translated and may contain errors or omissions.

Eino Study Notes — Appendix 2

Eino Study Notes — Appendix 2: the Components module

Mon Jul 20 2026
1971 words · 14 minutes

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 orchestration

1. 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 models
gpt4o, _ := 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 userID
router := 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

ScenarioRouting rulePurpose
A/B testinguserID hash → 10% new model, 90% old modelCompare the results, then decide on a full switch
Cost optimizationmessage length < 100 → cheap model; ≥ 100 → strong modelSimple questions don’t need a sledgehammer
Canary release1% 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 calls
chatModel, _ := 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

OptionEffectDefault
WithPrintAuth(false)Don’t print the Authorization headerRedacted
WithMaskHeaders(...)Extra headers to redact—
WithStreamLogging(true)Log streaming responses chunk by chunkNot logged
WithMaxStreamLogBytes(n)Max bytes of the streaming log8192
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 automatically
Variant 1: "best tourist attractions for families" ← from the "family" angle
Variant 2: "outdoor sightseeing spots recommendations" ← from the "outdoor" angle
Variant 3: "popular travel destinations and landmarks" ← from the "famous sights" angle
↓ search the same vector store with each
Result 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 list

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 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_vdb
User asks "is this product any good?" → search review_vdb
User asks "how do I return it?" → search faq_vdb

Router 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

DimensionMulti-QueryRouter
What it solvesThe user’s phrasing is too vague, the store can’t find itSeveral vector stores, no idea which one to search
Retrieval countN variants × 1 retriever = N callsM selected retrievers × 1 call = M calls
Core ideaRewrite the query with an LLM, search from several anglesAnalyze the query, dispatch to the matching store
FitsA single knowledge base, raising recallSeveral 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 path

5.3 Custom Parser

When the built-in TextParser / HTML / PDF doesn’t fit the need (parsing Word documents, Excel sheets, a custom format):

  1. Define the custom options struct (type options struct{...})
  2. Build the Option function with parser.WrapImplSpecificOptFn
  3. Define the Config + struct + constructor
  4. 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 options
  • parser.WrapImplSpecificOptFn wraps a custom func(*options) into a parser.Option

6. Panoramic Quick Reference

Component typeWhat it solvesSectionCore API
ABRouterAfraid to switch to the new model wholesale → split traffic by rule for an A/B comparison1NewABRouterChatModel(routerFn)
CurlRTThe API broke and you don’t know what was sent → cURL-style log debugging2NewCurlRT(transport, opts...)
Multi-QueryThe user’s search is too vague → the LLM rewrites N variants and searches each3multiquery.NewRetriever({RewriteHandler, OrigRetriever})
Router RetrieverSeveral vector stores, no idea which to search → auto-route by query content4router.NewRetriever({Retrievers, Router})
ExtParserMany document formats → auto-dispatch by extension5NewExtParser({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" // PDF

Layered 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:


Thanks for reading! Follow me if you'd like~

Eino Study Notes — Appendix 2

Mon Jul 20 2026
1971 words · 14 minutes