跳至内容

M11 Eino 框架

在之前的知识篇中,我们已经用 Go 完成模型接入、提示词、Agent 循环、Tool、RAG、多 Agent、上下文治理、可观测、评估与安全。本章开始使用 CloudWeGo Eino,把这些手写能力映射到标准组件、类型安全的编排系统和可复用的 Agent 范式中。

学习框架的目标,是在了解原理的基础上把已经理解的机制交给框架统一实现,减少重复性劳动,提升开发效率。

Eino 目前仍在快速演进,具体内容请以官方最新稳定版为准。 更多内容请访问 CloudWeGo 官方 Eino 文档。本章配套代码基于 github.com/cloudwego/eino v0.9.12github.com/cloudwego/eino-ext/components/model/openai v0.1.13

11.1 Eino 框架介绍

Eino 解决什么问题

开发一个大模型应用,通常要完成模型接入、消息管理、提示词渲染、Tool 调用、文档加载与检索、流程控制、全链路流式输出,以及日志、追踪和评估。随着应用规模增长,不同模块会重复定义消息、流、回调、工具 Schema、节点状态和错误处理,替换模型或存储实现时也容易牵动业务代码。

Eino 将共同能力抽象为稳定接口,把控制流与数据流交给编排引擎,再把成熟的 Agent 执行模式封装进 ADK。它是一套 Go SDK,模型服务、向量数据库、业务系统和观测平台仍由应用自行选择。

四层框架总览

官方文档中有以下 Eino 框架架构图,我们可以从四层视角理解 Eino:

eino 框架架构

层级关注点代表能力
组件层单一原子能力ChatModel、Tool、Retriever
编排层确定性执行结构与复用模式Chain、Graph、Workflow、Flow
Agent 层自主决策范式ReAct、Runner、多 Agent
DevOps 层开发与运行生命周期调试、Trace、评估、Prompt 治理

组件 → 编排 → Agent 构成业务执行能力的单向依赖,下层向上层提供标准化能力,上层封装更高阶范式。DevOps 作为横切层观察和治理前三层,不进入订单、检索和工单等业务控制流。EinoExt 是扩展实现仓库,覆盖具体组件、Callback handler 与 DevOps 工具。开发者可以按需使用任意层级:

  • 单次摘要任务只使用 ChatModel;
  • 固定的 RAG 问答使用 Chain;
  • 有路由、并行检索和循环改写的任务使用 Graph;
  • 多查询检索、父子文档或预制 Agent 模式可以使用 Flow;
  • 需要模型自主选择 Tool 的任务使用 ADK;
  • 开发期加入 Eino Dev,生产期接入 Trace 和评估。

Eino、EinoExt

仓库主要内容
cloudwego/einoschema、组件接口、编排、Flow、Callback、ADK
cloudwego/eino-ext模型、向量库、Tool、Callback handler、DevOps 等实现

Eino 核心仓库提供抽象和运行机制,EinoExt 提供面向具体供应商与基础设施的实现。项目引入 OpenAI ChatModel 时,业务代码依赖 Eino 的 Model 接口,装配代码创建 EinoExt 的 OpenAI 实现。

大模型调用示例

创建项目:

mkdir eino-hello
cd eino-hello
go mod init eino-hello
go get github.com/cloudwego/eino
go get github.com/cloudwego/eino-ext/components/model/openai

配置一个 OpenAI-compatible 服务,我们可以使用 DeepSeek 或豆包等兼容 OpenAI 风格的模型:

export DEEPSEEK_API_KEY="your-api-key"
export DEEPSEEK_BASE_URL="https://api.deepseek.com"
export DEEPSEEK_MODEL="deepseek-v4-pro"

完整程序:

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/cloudwego/eino-ext/components/model/openai"
	"github.com/cloudwego/eino/schema"
)

func main() {
	ctx := context.Background()

	cm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
		APIKey:  os.Getenv("DEEPSEEK_API_KEY"),
		BaseURL: os.Getenv("DEEPSEEK_BASE_URL"),
		Model:   os.Getenv("DEEPSEEK_MODEL"),
	})
	if err != nil {
		log.Fatal(err)
	}

	input := []*schema.Message{
		schema.SystemMessage("你是一个Go语言编程助手,擅长回答Go语言相关技术问题。"),
		schema.UserMessage("用两句话介绍 Eino 框架。"),
	}

	output, err := cm.Generate(ctx, input)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(output.Content)
}

这段代码只使用组件层:

OpenAI ChatModel 实现
[]*schema.Message
        ↓ Generate
*schema.Message

这段代码只调用 ChatModel。Chain、Graph、Flow、Agent 与 DevOps 能力可以随着项目需要逐步加入。

流式调用

reader, err := cm.Stream(ctx, input)
if err != nil {
	log.Fatal(err)
}
defer reader.Close()

for {
	chunk, err := reader.Recv()
	if errors.Is(err, io.EOF) {
		break
	}
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(chunk.Content)
}

流式调用需要遵守三个规则:

  1. reader 创建成功后及时 Close
  2. io.EOF 表示本次流正常结束;
  3. 保存历史或进入非流式节点前,要把所有 chunk 合并成完整消息。

11.2 组件层

组件层是整个框架的最底层,所有上层能力的原子执行单元。把 LLM 应用中所有异构的外部能力、原子操作,全部抽象成统一的标准接口,屏蔽底层实现差异。组件可以直接调用,也可以成为 Chain、Graph、Workflow 和 ADK 的节点。

分类组件
对话处理ChatModel、ChatTemplate
工具执行Tool、ToolsNode
文档与知识Loader、Transformer、Embedding、Indexer、Retriever
自定义逻辑Lambda

每类组件通常包含接口、公共 Option、实现专属 Option 和 Callback 数据结构。接口定义位于 Eino 核心,面向大模型供应商和基础设施的实现主要位于 EinoExt。

11.2.1 ChatModel

ChatModel 以 schema.Message 的切片作为输入,并返回响应消息——可以完整返回,也可以以流的形式增量返回。它是 eino 流水线中最基本的构建模块:每个与 LLM 通信的应用程序都必须通过此接口。

推荐阅读官方文档中的ChatModel 使用说明

接口

BaseModel 是通用基础模型接口,由消息类型 M 参数化。

它公开两种交互模式:

  • [BaseModel.Generate]:阻塞,直到模型返回完整响应。
  • [BaseModel.Stream]:返回一个 [schema.StreamReader],该读取器会随着模型生成消息而逐步返回消息块。
type BaseModel[M messageType] interface {
	Generate(ctx context.Context, input []M, opts ...Option) (M, error)

	Stream(ctx context.Context, input []M, opts ...Option) (*schema.StreamReader[M], error)
}

type BaseChatModel = BaseModel[*schema.Message]

需要预绑定工具时使用 ToolCallingChatModel

type ToolCallingChatModel interface {
	BaseChatModel
	WithTools(tools []*schema.ToolInfo) (ToolCallingChatModel, error)
}

WithTools 返回新实例,适合并发请求使用不同工具集合。

旧版本的 ChatModel 使用 BindTools 会修改原实例,v0.9 已标记为 Deprecated。

上述 ChatModel 可用于对话、文本生成、分类、抽取、多模态输入和 ToolCall 生成。

Message

Message 表示模型输入和输出的数据结构,可来自用户输入或模型返回。它支持纯文本和多模态内容。

字段含义
Rolesystem、user、assistant、tool
Content文本内容
UserInputMultiContent用户输入的文本、图片、音频、视频、文件等有类型内容
AssistantGenMultiContent模型生成的文本、图片、音频、视频等有类型内容
ToolCallsassistant 请求执行的工具
ToolCallIDtool result 对应的调用 ID
ToolName工具名称
ReasoningContent推理模型返回的 reasoning 内容
ResponseMetatoken、结束原因等元数据
Extra组件实现附加的供应商信息

旧版本的字段 MultiContent 在 v0.9.12 已标记 Deprecated。新代码按输入与输出方向分别使用 UserInputMultiContentAssistantGenMultiContent,从而避免同一字段承担两套语义。

常用快捷构造函数:

schema.SystemMessage("你是客服助手。")
schema.UserMessage("查询订单 ORD-1001")
schema.AssistantMessage("请提供订单号", nil)
schema.ToolMessage(
	"{\"status\":\"shipped\"}",
	"call_123",
	schema.WithToolName("query_order"),
)

Generate 与 Stream

Generate 返回完整消息,Stream 返回增量消息流。要根据使用场景选择合适的方法。

场景推荐方式
单元测试、后台批任务Generate
CLI 或 Web 对话Stream
结构化抽取Generate
长报告生成Stream
下游一次解析完整 JSONGenerate,或先收集 Stream

请求级 Option

Model 组件提供了一组公共 Option 用于配置模型行为:

answer, err := cm.Generate(
	ctx,
	messages,
	model.WithTemperature(0.2),        // 设置温度
	model.WithMaxTokens(1200),         // 设置最大 token 数
	model.WithTopP(0.9),               // 设置 top_p 值
	model.WithStop([]string{"<END>"}), // 设置停止词
)

模型公共 Option 还包括:

Option作用与边界
model.WithModel为本次调用选择模型;具体实现需要支持运行时切换
model.WithTools为本次调用传入 ToolInfo;ChatModelAgent 使用这一入口绑定工具
model.WithToolChoice约束普通 ChatModel 的工具选择,可同时指定允许的工具名
model.WithDeferredTools注册延迟加载工具,供 provider 原生 Tool Search 使用
model.WithToolSearchTool注册 provider 原生工具搜索入口;该工具不重复放入 WithTools
model.WithAgenticToolChoiceAgenticModel 专用的工具选择参数

公共 Option 只定义跨实现的数据契约。模型供应商缺少对应能力时,具体 EinoExt 实现可能忽略参数或返回错误,接入测试需要覆盖实际 provider。

11.2.2 ChatTemplate

ChatTemplate 将变量 map 格式化为 ChatModel 的消息列表,为大模型调用准备上下文。

推荐阅读官方文档中的 ChatTemplate 使用说明

接口与模板语法

Format 将变量 vs 中的值替换到模板的消息列表中,并返回生成的 []*schema.Message。如果模板中存在但 vs 中不存在的变量键,则会产生运行时错误——没有编译时安全性。建议在模板和调用者之间保持一致的变量命名。

type ChatTemplate interface {
	Format(ctx context.Context, vs map[string]any, opts ...Option) ([]*schema.Message, error)
}

具体的替换语法(FString、GoTemplate、Jinja2)在构造时确定。

格式变量语法特点
schema.FString{question}简洁,适合大部分 Prompt
schema.GoTemplate{{.question}}支持条件与循环
schema.Jinja2{{ question }}适合迁移 Jinja 模板

创建并填充模板

Prompt 组件是一个用于处理和格式化提示词模板的组件。它的主要作用是将用户提供的变量值填充到预定义的消息模板中,生成用于与大语言模型交互的标准消息格式。

这个组件可用于以下场景:

  • 构建结构化的系统提示
  • 处理多轮对话的模板 (包括 history)
  • 实现可复用的提示模式
tpl := prompt.FromMessages(
	schema.FString,
	schema.SystemMessage(
		"你是{brand}客服。只依据已知资料回答。",
	),
	schema.MessagesPlaceholder("history", true),
	schema.UserMessage(
		"资料:\n{context}\n\n问题:{question}",
	),
)

messages, err := tpl.Format(ctx, map[string]any{
	"brand":    "Aurora",
	"history":  history,
	"context":  retrievedContext,
	"question": userQuestion,
})

MessagesPlaceholder[]*schema.Message 插入指定位置。第二个参数为 true 时,该变量可以省略。

在 Chain 中使用

在编排场景下通常把 ChatTemplate 作为 ChatModel 的前置节点。

chain := compose.NewChain[map[string]any, *schema.Message]()
chain.
	AppendChatTemplate(tpl).
	AppendChatModel(cm)

app, err := chain.Compile(ctx)
if err != nil {
	log.Fatalf("invoke failed, err:%v", err)
}
answer, err := app.Invoke(ctx, variables)

其他常用创建 messages 方法:

方法 / 结构体功能说明
schema.Message{}schema.Message 是实现了 Format 接口的结构体,可直接构建 schema.Message{} 作为 template
schema.SystemMessage()构建 role 为 system 的 message 的快捷方法
schema.AssistantMessage()构建 role 为 assistant 的 message 的快捷方法
schema.UserMessage()构建 role 为 user 的 message 的快捷方法
schema.ToolMessage()构建 role 为 tool 的 message 的快捷方法
schema.MessagesPlaceholder()可用于把一个 []*schema.Message 插入到 message 列表中,常用于插入历史对话

11.2.3 Tool 与 ToolsNode

Tool 是模型可选择调用的外部能力,例如订单查询、搜索、计算、数据库、MCP Tool 或子 Agent。

ToolsNode 是 eino 框架指定的"Tool 执行器",无论是 Graph 内还是 Agent 中,Tool 的执行都要通过 ToolsNode。

模型负责选择要执行的 Tool,ToolsNode 负责解析 ToolCall 并执行。

推荐阅读官方文档中的:

Tool 接口

Tool 组件提供了两类接口:标准工具接口和增强型工具接口。

标准工具接口

标准工具接口返回字符串类型的结果:

// BaseTool 基础工具接口,提供工具信息
type BaseTool interface {
	Info(ctx context.Context) (*schema.ToolInfo, error)
}

// InvokableTool 可调用的工具接口,支持同步调用
type InvokableTool interface {
	BaseTool
	InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)
}

// StreamableTool 支持流式输出的工具接口
type StreamableTool interface {
	BaseTool
	StreamableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (*schema.StreamReader[string], error)
}

Info 返回名称、描述和输入 JSON Schema。模型依据这些信息决定调用时机并生成参数。

普通 Tool 使用 JSON 字符串输入和字符串结果,适合绝大多数业务 API。

增强型工具接口(Enhanced Tool)

Enhanced Tool 使用 ToolArgumentToolResult,结果可以包含文本、图片、音频、视频和文件。

// EnhancedInvokableTool 是支持返回结构化多模态结果的工具接口
// 与返回字符串的 InvokableTool 不同,此接口返回 *schema.ToolResult
// 可以包含文本、图片、音频、视频和文件
type EnhancedInvokableTool interface {
	BaseTool
	InvokableRun(ctx context.Context, argument *schema.ToolArgument, opts ...Option) (*schema.ToolResult, error)
}

// EnhancedStreamableTool 是支持返回结构化多模态结果的流式工具接口
// 提供流式读取器以逐步访问多模态内容
type EnhancedStreamableTool interface {
	BaseTool
	StreamableRun(ctx context.Context, argument *schema.ToolArgument, opts ...Option) (*schema.StreamReader[*schema.ToolResult], error)
}

ToolsNode 为 Enhanced 接口提供同步与流式适配;某个工具存在 Enhanced 执行入口时,运行时优先走结构化结果链路。

创建工具

创建工具有4种方式。

  • 直接实现接口
  • 将本地函数转为 tool
  • 使用 eino-ext 中提供的 社区 tool 实现
  • 使用 MCP 协议

下面演示常用的将本地函数转为 tool,可以使用 NewTool 创建工具,更推荐使用 InferTool 创建工具。

type OrderArgs struct {
	OrderID string `json:"order_id" jsonschema:"required" jsonschema_description:"订单号,例如 ORD-1001"`
}

type Order struct {
	OrderID string `json:"order_id"`
	Info    string `json:"info"`
	Status  int    `json:"status"`
}

func QueryOrder(ctx context.Context, args OrderArgs) (*Order, error) {
	customerID := auth.CustomerID(ctx)
	order, err := orderService.Query(
		ctx,
		customerID,
		args.OrderID,
	)
	if err != nil {
		return nil, err
	}
	return order, nil
}

queryOrderTool, err := utils.InferTool(
	"query_order",
	"查询当前登录客户的订单,包含商品信息、订单状态、金额和运单号等。",
	QueryOrder,
)

工具名要稳定且简短。描述要讲清调用条件、参数语义、返回数据和限制。

InferTool 根据输入 struct 和 jsonschema tag 生成参数 Schema,自动反序列化模型参数,并把返回值序列化为字符串。需要多模态结果时使用 utils.InferEnhancedTool;需要读取调用级 tool.Option 时使用 InferOptionableToolInferOptionableEnhancedTool

ToolsNode

ToolsNode 是 eino 框架指定的"Tool 执行器",无论是 Graph 内还是 Agent 中,Tool 的执行都要通过 ToolsNode:

// compose/tool_node.go

// run tools using `Invoke`
func (tn *ToolsNode) Invoke(ctx context.Context, input *schema.Message,
    opts ...ToolsNodeOption) ([]*schema.Message, error)
    
// run tools using `Stream`
func (tn *ToolsNode) Stream(ctx context.Context, input *schema.Message,
    opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.Message], error)

给 ToolsNode 配置一个 Tool 列表以及一些配套策略:

// compose/tool_node.go

type ToolsNodeConfig struct {
    Tools []tool.BaseTool

    UnknownToolsHandler func(ctx context.Context, name, input string) (string, error)

    ExecuteSequentially bool

    ToolArgumentsHandler func(ctx context.Context, name, arguments string) (string, error)

    ToolCallMiddlewares []ToolMiddleware
}

这样 ToolsNode 就“能够执行配置的 Tool”,并获得一些扩展能力,如执行时序、异常处理、入参处理、middleware 扩展等。

ToolsNode 如何“决策”应该执行哪个 Tool?它不决策,而是依据输入的 *schema.Message 来执行:

// schema/message.go

type Message struct {
    // role should be 'assistant' for tool call message
    Role RoleType `json:"role"`

    // here each `ToolCall` is generated by ChatModel and to be executed by ToolsNode
    ToolCalls []ToolCall `json:"tool_calls,omitempty"`
    
    // other fields...
}

// ToolCall is the tool call in a message.
// It's used in Assistant Message when there are tool calls should be made.
type ToolCall struct {
    // Index is used when there are multiple tool calls in a message.
    // In stream mode, it's used to identify the chunk of the tool call for merging.
    Index *int `json:"index,omitempty"`
    // ID is the id of the tool call, it can be used to identify the specific tool call.
    ID string `json:"id"`
    // Type is the type of the tool call, default is "function".
    Type string `json:"type"`
    // Function is the function call to be made.
    Function FunctionCall `json:"function"`

    // Extra is used to store extra information for the tool call.
    Extra map[string]any `json:"extra,omitempty"`
}

// FunctionCall is the function call in a message.
// It's used in Assistant Message.
type FunctionCall struct {
    // Name is the name of the function to call, it can be used to identify the specific function.
    Name string `json:"name,omitempty"`
    // Arguments is the arguments to call the function with, in JSON format.
    Arguments string `json:"arguments,omitempty"`
}

ChatModel(LLM) 生成要调用的 []ToolCall(包含 ToolName,Argument 等),放到 *schema.Message 中传给 ToolsNode。ToolsNode 针对每个 ToolCall 实际执行一次调用。

如果配置了 ExecuteSequentially,则 ToolsNode 会按照 []ToolCall 中的先后顺序来执行工具。

每个 ToolCall 调用完成后的结果,又会封装为 *schema.Message,作为 ToolsNode 输出的一部分。

toolsNode, err := compose.NewToolNode(
	ctx,
	&compose.ToolsNodeConfig{
		Tools: []tool.BaseTool{
			queryOrderTool,
			xxxTool,
		},
		ExecuteSequentially: true,
	},
)

ToolsNodeConfig 在 v0.9.12 中包含以下运行控制点:

配置行为
Tools注册可执行 Tool;每一项还需实现普通或 Enhanced 的同步/流式执行接口
ExecuteSequentiallyfalse 时并发执行同一 assistant message 中的多个 ToolCall,默认值为 false
UnknownToolsHandler处理模型生成的未知工具名;未配置时返回错误
ToolAliases把工具名别名与顶层参数别名映射到规范名称
ToolArgumentsHandler在执行前统一改写或校验 JSON 参数
ToolCallMiddlewares分别包装普通与 Enhanced 的同步/流式调用

参数别名只处理 JSON 顶层 key。别名冲突、空别名和包含 . 的嵌套字段映射会在构造阶段返回错误。

调用 tool

大模型调用工具的完整流程:

  1. Tool.Info → 工具定义发送给模型
  2. 模型生成 assistant.ToolCalls
  3. ToolsNode 按名称查找 Tool
  4. 解析 arguments JSON 并调用
  5. 结果转成 role=tool 的 Message
  6. ToolCall 与结果一起送回模型

大模型返回的同一消息中可能包含多个 ToolCall。并行执行适合相互独立的查询;如果存在顺序依赖、写冲突、事务或限流约束时则应使用 ExecuteSequentially=true

本地可以通过 mock 大模型消息实现调用工具。

// Mock LLM 输出作为输入
input := &schema.Message{
	Role: schema.Assistant,
	ToolCalls: []schema.ToolCall{
		{
			Function: schema.FunctionCall{
				Name:      "query_order",
				Arguments: `{"order_id": "lwz-1234567"}`,
			},
		},
	},
}

toolMessages, err := toolsNode.Invoke(ctx, input)

ToolsNode 在调用 Invoke时的 Option 参数支持 compose.WithToolOptionWithToolListWithToolAliases。动态替换执行工具列表时,还要让模型本轮看到完全一致的 ToolInfo 集合;ADK 可以在 BeforeAgentBeforeModelRewriteState 中统一维护两侧数据。

11.2.4 Document Loader

Document Loader 是一个用于加载文档的组件。它的主要作用是从不同来源(如网络 URL、本地文件等)加载文档内容,并将其转换为标准的文档格式。这个组件在处理需要从各种来源获取文档内容的场景中发挥重要作用,比如:

  • 从网络 URL 加载网页内容
  • 读取本地 PDF、Word 等格式的文档
type Loader interface {
	Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error)
}

type Source struct {
	URI string
}

统一输出:

type Document struct {
	ID       string
	Content  string
	MetaData map[string]any
}

Metadata 通常保存标题、来源 URI、版本、更新时间、作者、部门、语言、tenant、ACL、MIME type 和页码。

Loader:定位、下载或打开数据
Parser:解析 PDF、HTML、DOCX、Markdown
Document:统一输出

使用示例:

docs, err := docLoader.Load(ctx, document.Source{
	URI: "/data/refund-policy.pdf",
})

工程要求:

  • 限制 URL 协议与目标网段,防止 SSRF;
  • 限制本地路径,防止任意文件读取;
  • 设置下载大小、页数、超时和并发上限;
  • 使用稳定 Document ID 支持幂等导入;
  • 保存 source、version、tenant 和 ACL;
  • 区分下载失败、解析失败和空文档;
  • 文档内容进入 Prompt 前继续执行注入防护。

11.2.5 Document Transformer

Document Transformer 是一个用于文档转换和处理的组件。它的主要作用是对输入的文档进行各种转换操作,如分割、过滤、合并等,从而得到满足特定需求的文档。这个组件可用于以下场景中:

  • 将长文档分割成小段落以便于处理
  • 根据特定规则过滤文档内容
  • 对文档内容进行结构化转换
  • 提取文档中的特定部分
type Transformer interface {
	Transform(ctx context.Context, src []*schema.Document, opts ...TransformerOption) ([]*schema.Document, error)
}

RAG 中的典型流程:

父文档
  → 去除导航、页眉和页脚
  → 按标题或段落切分
  → 控制 chunk token 长度
  → 添加 overlap
  → 继承元数据
  → chunk Documents

每个 chunk 至少保留:

字段用途
parent_doc_id回溯父文档
chunk_index恢复原始顺序
title生成引用
source定位来源
version判断内容版本
tenant/acl检索权限过滤

较小 chunk 定位更准确,也更容易丢失完整语义;较大 chunk 保存语境,同时增加噪声和 token 成本。chunk size 与 overlap 需要通过真实问题集评估。

11.2.6 Embedding

Embedding 组件是一个用于将文本转换为向量表示的组件。它的主要作用是将文本内容映射到向量空间,使得语义相似的文本在向量空间中的距离较近。这个组件在以下场景中发挥重要作用:

  • 文本相似度计算
  • 语义搜索
  • 文本聚类分析
type Embedder interface {
	EmbedStrings(ctx context.Context, texts []string, opts ...Option) ([][]float64, error)
}

使用示例:

vectors, err := embedder.EmbedStrings(ctx, []string{
	"七天无理由退货政策",
	"订单物流查询",
})

Embedding 主要用于语义搜索、相似度计算、聚类与去重。RAG 入库和查询必须遵守同一向量契约:

  • provider 与模型名;
  • 向量维度;
  • 是否归一化;
  • query/document 前缀;
  • 截断策略;
  • cosine、inner product 或 L2 距离;
  • 模型与预处理版本。

入库和查询使用不兼容模型会破坏相似度。模型、维度或预处理变化后,应新建索引或重建现有索引。生产实现还需处理批大小、限流、缓存、重试、向量数量和维度校验。

11.2.7 Indexer

Indexer 组件是一个用于存储和索引文档的组件。它的主要作用是将文档及其向量表示存储到后端存储系统中,并提供高效的检索能力。这个组件在以下场景中发挥重要作用:

  • 构建向量数据库,以用于语义关联搜索
type Indexer interface {
	Store(ctx context.Context, docs []*schema.Document, opts ...Option) (ids []string, err error)
}

使用 Doubao Embedding 模型存储至本地 Docker-Compose Milvus 示例:

推荐直接阅读官方文档: Docker Compose 启动 Milvus Eino-Ext Milvus2 示例

import (
	"github.com/cloudwego/eino-ext/components/embedding/ark"
	"github.com/cloudwego/eino-ext/components/indexer/milvus2"
	"github.com/cloudwego/eino/schema"
	"github.com/milvus-io/milvus/client/v2/milvusclient"
)


// 创建 embedding 模型
emb, err := ark.NewEmbedder(ctx, &ark.EmbeddingConfig{
	APIKey:     os.Getenv("DOUBAO_API_KEY"),
	Model:      "doubao-embedding-vision-251215",
	APIType:    new(ark.APITypeMultiModal),
	Dimensions: &defaultDim,
})
if err != nil {
	log.Fatalf("Failed to create embedding: %v", err)
	return err
}
// 创建索引器
indexer, err := milvus2.NewIndexer(ctx, &milvus2.IndexerConfig{
	ClientConfig: &milvusclient.ClientConfig{
		Address: "localhost:19530",
	},
	Collection: "my_collection",
	Vector: &milvus2.VectorConfig{
		Dimension:    2048,
		MetricType:   milvus2.COSINE,
		IndexBuilder: milvus2.NewAutoIndexBuilder(),
	},
	Embedding: emb,
})
if err != nil {
	slog.WarnContext(ctx, "newIndexer failed", "err", err)
	return err
}

// 索引文档
docs := []*schema.Document{
	{
		ID:      "doc1",
		Content: "EINO 是一个用于构建人工智能应用的框架",
		MetaData: map[string]any{
			"category": "database",
			"year":     2021,
		},
	},
	{
		ID:      "doc2",
		Content: "Aurora品牌耳机拆封后概不退货。",
		MetaData: map[string]any{
			"category": "database",
			"year":     2026,
		},
	},
	{
		ID:      "doc3",
		Content: "《宠物厌食症状识别与干预》 —— 当宠物连续 24 小时拒食或显著减食,常见原因有应激反应、消化系统疾病、口腔问题……",
		MetaData: map[string]any{
			"category": "database",
			"year":     2013,
		},
	},
}
ids, err := indexer.Store(ctx, docs)
if err != nil {
	slog.WarnContext(ctx, "indexer store failed", "err", err)
	return err
}
fmt.Printf("ids:%#v\n", ids)
return nil
}

公共 Option 可传入子索引和 Embedder:

ids, err := idx.Store(
	ctx,
	docs,
	indexer.WithSubIndexes([]string{"customer_service"}),
	indexer.WithEmbedding(embedder),
)

Indexer 实现通常执行:

  1. 校验 ID、Content 与 Metadata;
  2. 根据 Content 生成向量;
  3. 映射后端字段;
  4. 批量写入;
  5. 返回成功文档 ID;
  6. 处理幂等、部分失败和重试;
  7. 保存索引、数据和 embedding 版本。

稳定 ID 决定重复导入是更新还是生成重复数据。批量接口遇到部分失败时,要明确成功集合、失败集合和重试范围。

11.2.8 Retriever

Retriever 组件是一个用于从各种数据源检索文档的组件。它的主要作用是根据用户的查询(query)从文档库中检索出最相关的文档。这个组件在以下场景中特别有用:

  • 基于向量相似度的文档检索
  • 基于关键词的文档搜索
  • 知识库问答系统 (rag)
type Retriever interface {
	Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error)
}

从上面启动的 Milvus 环境调用示例:

import (
	"github.com/cloudwego/eino-ext/components/embedding/ark"
	"github.com/cloudwego/eino-ext/components/retriever/milvus2"
	"github.com/cloudwego/eino-ext/components/retriever/milvus2/search_mode"
	"github.com/milvus-io/milvus/client/v2/milvusclient"
)
// 创建 embedding 模型
emb, err := ark.NewEmbedder(ctx, &ark.EmbeddingConfig{
	APIKey:     os.Getenv("DOUBAO_API_KEY"),
	Model:      "doubao-embedding-vision-251215",
	APIType:    new(ark.APITypeMultiModal),
	Dimensions: &defaultDim,
})
if err != nil {
	log.Fatalf("Failed to create embedding: %v", err)
	return
}

// 创建 retriever
retriever, err := milvus2.NewRetriever(ctx, &milvus2.RetrieverConfig{
	ClientConfig: &milvusclient.ClientConfig{
		Address: "localhost:19530",
	},
	Collection: "my_collection",
	TopK:       10,
	SearchMode: search_mode.NewApproximate(milvus2.COSINE),
	Embedding:  emb,
})
if err != nil {
	log.Fatalf("Failed to create retriever: %v", err)
	return
}
log.Printf("Retriever created successfully")

// 检索文档
documents, err := retriever.Retrieve(ctx, "耳机拆了试戴不合适能退吗?")

Retriever 可以封装向量检索、关键词检索、混合检索、多 query 检索和 rerank。上层只依赖统一返回类型。

建议在 Metadata 中保存:

  • title、source、URI、chunk ID;
  • dense score、BM25 score、rerank score;
  • tenant、ACL、版本和生效时间;
  • retriever 名称与 query rewrite 结果。

不同后端的 score 语义可能是距离、相似度或概率。TopK 与 ScoreThreshold 需要针对具体实现校准。回答置信度还要结合来源质量、文档一致性与生成结果评估。

权限过滤必须在 Retriever 或存储查询层强制执行。把权限条件写进 Prompt 无法形成安全边界。

完整 RAG 检索链路示例见课件代码中的 rag_demo

11.2.9 Lambda

官方给你准备好了 ChatModel(大模型)、ModelTemplate(提示词模板)、Retriever(向量检索)这些现成的标准零件;但业务里总有一些逻辑是官方没做的,Lambda 是 Eino 中最基础的组件类型,它可以把普通 Go 函数包装成一个和官方组件「规格完全一致」的节点,能和其他组件一样被编排运行。

Lambda 组件由 4 种执行函数组成,这些函数根据输入和输出是否为流式而有所不同,分别对应 4 种交互模式:调用 (Invoke)、流式传输 (Stream)、收集 (Collect) 和转换 (Transform)。

Lambda 组件

Lambda 组件的核心是 Lambda 结构体,它包装了用户提供的 Lambda 函数,用户可通过构建方法创建一个 Lambda 组件:

// eino/compose/types_lambda.go

type Lambda struct {
    executor *composableRunnable
}

Lambda 支持的四种函数类型定义如下,即用户提供的 Lambda 函数需要满足这些函数签名:

type Invoke[I, O, TOption any] func(ctx context.Context, input I, opts ...TOption) (O, error)

type Stream[I, O, TOption any] func(ctx context.Context, input I, opts ...TOption) (*schema.StreamReader[O], error)

type Collect[I, O, TOption any] func(ctx context.Context, input *schema.StreamReader[I], opts ...TOption) (O, error)

type Transform[I, O, TOption any] func(ctx context.Context, input *schema.StreamReader[I], opts ...TOption) (*schema.StreamReader[O], error)

四种构造函数:

构造函数数据形态用途
InvokableLambda完整值 → 完整值转换、校验、解析
StreamableLambda完整值 → 流分批生成
CollectableLambda流 → 完整值合并和统计
TransformableLambda流 → 流增量过滤与改写

下面的示例是,把一个格式化检索结果的函数转为 Lambda:

import (
	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/schema"
)
formatDocs := compose.InvokableLambda(
	func(_ context.Context, docs []*schema.Document) (string, error) {
		var b strings.Builder
		for i, doc := range docs {
			fmt.Fprintf(&b, "[%d] %s\n", i+1, doc.Content)
		}
		return b.String(), nil
	},
)

// Lambda 是 Chain/Graph 中的节点,需要先加入 Chain 并编译。
chain := compose.NewChain[[]*schema.Document, string]()
chain.AppendLambda(formatDocs)

runnable, err := chain.Compile(ctx)
if err != nil {
	return fmt.Errorf("compile chain: %w", err)
}

docs := []*schema.Document{
	{ID: "doc-1", Content: "Eino 是一个大模型应用开发框架。"},
	{ID: "doc-2", Content: "Lambda 可以包装自定义的数据处理函数。"},
}

result, err := runnable.Invoke(ctx, docs)
if err != nil {
	return fmt.Errorf("invoke chain: %w", err)
}

fmt.Println(result)

Lambda 适合类型转换和编排胶水。复杂领域规则放在独立 Go package 中,Lambda 负责调用和适配,单元测试可以脱离编排引擎运行。

11.2.10 AgenticXxx 系列

schema.Message 的结构来自 Chat Completions 风格(纯文本),并在迭代中加入了 reasoning、多模态与工具搜索等字段:

Content
ReasoningContent
UserInputMultiContent / AssistantGenMultiContent
ToolCalls / ToolCallID
ResponseMeta / Extra

新一代 provider agentic API 的一次响应可能依次包含:

  • 多段 reasoning 或带签名的推理内容;
  • 文本、图片、音频和视频输出;
  • 多次本地 function tool 调用;
  • provider 服务端执行的 web search 等内置工具;
  • provider 托管的 MCP Tool、结果和工具列表;
  • MCP 执行前的审批请求与审批结果;
  • provider 专属响应元数据。

这些字段按类别分开保存,难以表达 reasoning、文本、多个工具调用和 provider 内置工具在同一响应中的交错顺序。普通 Tool 的字符串结果也无法完整承载多模态数据,流式 chunk 的合并和跨 provider 回放随之变复杂。

Eino v0.9 新增 schema.AgenticMessage

type AgenticMessage struct {
	Role          AgenticRoleType
	ContentBlocks []*ContentBlock
	ResponseMeta  *AgenticResponseMeta
	Extra         map[string]any
}

消息结构从「扁平单文本」升级为「结构化内容块」,专门适配大模型厂商原生的 Agent 能力。每个 ContentBlock 表示一个有类型、可排序的内容单元,例如:

Block表达内容
Reasoning推理文本或签名
UserInputText/Image/Audio/Video/File用户多模态输入
AssistantGenText/Image/Audio/Video模型多模态输出
FunctionToolCall/Result本地函数工具
ServerToolCall/Resultprovider 内置工具
MCPToolCall/Resultprovider 托管的 MCP 调用
MCPListToolsResultprovider 返回的 MCP 工具列表
MCPToolApprovalRequest/ResponseMCP 审批
ToolSearchResultprovider 或动态工具中间件返回的工具发现结果

因此形成一条新的类型链:

传统组件Agentic 组件
schema.Messageschema.AgenticMessage
ChatTemplateAgenticChatTemplate
ChatModelAgenticModel
ToolsNodeAgenticToolsNode

AgenticModel

type AgenticModel = BaseModel[*schema.AgenticMessage]

AgenticModel 直接承接 OpenAI Responses API、Claude API、Gemini API 等 provider 的原生 agentic 数据。工具通过请求级 model.WithTools 传入。

AgenticChatTemplate

type AgenticChatTemplate interface {
	Format(ctx context.Context, vs map[string]any, opts ...Option) ([]*schema.AgenticMessage, error)
}

它负责为 AgenticModel 准备 system、user、assistant history 等结构化消息。Eino v0.9.12 中,AgenticRoleType 仅定义 systemuserassistant 三种角色;provider 的其他原生字段通过 ContentBlock、ResponseMeta 或 Extra 表达。

模板变量只渲染 UserInputText、UserInputImage、UserInputAudio、UserInputVideo、UserInputFile 这几类用户输入 block。reasoning、模型输出、Tool Call/Result、Tool Search Result、server tool 和 MCP block 会原样保留;历史通过 schema.AgenticMessagesPlaceholder 整体插入。这个限制可以保护模型输出和工具轨迹在历史回放时保持原值。

AgenticToolsNode

AgenticToolsNode 读取 FunctionToolCall block,把它适配成 ToolsNode 可执行的 ToolCall,再把结果转换成 user role 的 FunctionToolResult block。Enhanced Tool 的结果可以继续携带文本、图片、音频、视频和文件;工具搜索结果会转换成 ToolSearchResult block。

解决的核心问题

  1. 保留同一响应中各类内容的真实顺序;
  2. 结构化表达 reasoning、多模态、server tool、MCP 与审批;
  3. 减少 provider 原生协议到通用消息的有损转换;
  4. 明确流式合并、回放、持久化和观测的数据结构;
  5. 让 Tool Result 继续携带多模态内容。

普通聊天和传统 function calling 可以继续使用 Message 路线。需要 Responses API、原生 reasoning、服务端工具、托管 MCP 或多模态工具结果时,可以评估 Agentic 路线。

一条链中的 Template、Model、ToolsNode、Agent 和事件消费者必须使用同一消息类型。Agentic 系列在官方文档中仍标为 Beta,项目要锁定版本并为每个 provider 编写集成测试。

11.3 编排层

基础组件层提供了模型、检索、工具等原子能力。一个完整应用还要根据业务逻辑组合这些能力,这就是编排。

编排层集中解决:

  • 节点按照什么顺序执行;
  • 上游输出如何成为下游输入;
  • 哪些分支根据条件运行;
  • 哪些步骤可以并行;
  • 循环何时继续、何时结束;
  • 流式数据如何拼接、复制、合并与转发;
  • 节点级状态、Option 和 Callback 如何注入;
  • 执行中断后如何保存并恢复。

编排的基础模型

Eino 用节点表达数据流:

Node:Component、Lambda 或子图
Edge:执行依赖与数据通道
State:单次运行共享状态
Branch:运行时选择后继
Runnable:Compile 后的统一执行接口

Eino 强调上下游类型对齐。前一个节点的输出类型必须能够作为下一个节点的输入类型。Graph 在 Compile 阶段完成拓扑检查和一部分类型检查,将大量问题提前到装配期暴露。

编排代码通常经历三步:

声明节点 → 连接节点 → Compile → Invoke/Stream

11.3.1 Chain

Chain 属于链式有向图,最简单的线性流水线,适合固定顺序的简单流程:

输入 → ChatTemplate → ChatModel → Parser → 输出

示例:

tpl := prompt.FromMessages(
	schema.FString,
	schema.SystemMessage(
		"你是售后客服,只依据资料回答。",
	),
	schema.UserMessage(
		"资料:{context}\n问题:{question}",
	),
)

chain := compose.NewChain[
	map[string]any,
	*schema.Message,
]()

chain.
	AppendChatTemplate(tpl).
	AppendChatModel(cm)

app, err := chain.Compile(ctx)
if err != nil {
	return err
}

result, err := app.Invoke(ctx, map[string]any{
	"context":  "激活后的耳机不支持七天无理由退货。",
	"question": "耳机激活后还能无理由退货吗?",
})

数据类型依次变化:

map[string]any
  ↓ ChatTemplate
[]*schema.Message
  ↓ ChatModel
*schema.Message

Chain 是 Graph 的简化封装,适合向前推进的流程。固定工作流优先使用 Chain,可以减少节点 key 和 Edge 声明。

11.3.2 Graph

Graph 支持有向无环图和有向有环图,可以表达分支、并行、循环与状态机。

一个客服路由图:

                       ┌→ retrieve_policy ─┐
question → classify ──┼→ query_order ─────┼→ answer
                       └→ ask_detail ──────┘

创建最小 Graph:

g := compose.NewGraph[string, string]()

normalize := compose.InvokableLambda(
	func(ctx context.Context, input string) (string, error) {
		return strings.TrimSpace(input), nil
	},
)

reply := compose.InvokableLambda(
	func(ctx context.Context, input string) (string, error) {
		return "已收到:" + input, nil
	},
)

if err := g.AddLambdaNode("normalize", normalize); err != nil {
	return err
}
if err := g.AddLambdaNode("reply", reply); err != nil {
	return err
}

if err := g.AddEdge(compose.START, "normalize"); err != nil {
	return err
}
if err := g.AddEdge("normalize", "reply"); err != nil {
	return err
}
if err := g.AddEdge("reply", compose.END); err != nil {
	return err
}

app, err := g.Compile(ctx)

Graph 可以直接添加标准组件:

  • AddChatTemplateNode
  • AddChatModelNode
  • AddToolsNode
  • AddRetrieverNode
  • AddEmbeddingNode
  • AddIndexerNode
  • AddLoaderNode
  • AddDocumentTransformerNode
  • AddLambdaNode
  • AddGraphNode

条件分支

Branch 根据当前输入返回后继节点:

branch := compose.NewGraphBranch(
	func(
		ctx context.Context,
		msg *schema.Message,
	) (string, error) {
		if len(msg.ToolCalls) > 0 {
			return "tools", nil
		}
		return compose.END, nil
	},
	map[string]bool{
		"tools":     true,
		compose.END: true,
	},
)

if err := g.AddBranch("model", branch); err != nil {
	return err
}

并行

一个节点连接多个后继时,满足执行条件的后继可以并发运行。例如 RAG 可以同时执行 dense search、BM25 search 和用户画像加载,再在汇聚节点合并。

并行节点需要考虑:

  • 是否共享可变状态;
  • 下游何时触发;
  • 多路错误如何汇总;
  • 是否允许部分成功;
  • 每条分支的超时与并发上限。

循环

Graph 可以用回边表达循环:

generate → evaluate
   ↑          │
   └─ revise ←┘

循环必须定义:

  • 继续条件;
  • 成功条件;
  • 最大步数;
  • 超时;
  • 失败时返回什么;
  • 中间状态如何保存。

Workflow

Workflow 是一套编排的 API,与 Graph API 在架构上处于同一层:

    flowchart LR
  E[Eino compose engine]
  G[Graph API]
  W[Workflow API]
  C[Chain API]
  E --> G
  E --> W
  G --> C
  

Graph 编排时,要求“类型对齐”,而 Workflow 支持灵活的输入输出类型,实现了控制流和数据流分离的设计。

例如,适合多个上游输出按字段汇聚:

rewrite.query ───────────┐
retrieve.documents ──────┼→ AnswerInput
profile.user_profile ────┘

它支持 struct、map 与嵌套字段映射,并将控制依赖和数据映射分别表达。节点较多、输入结构复杂时,Workflow 可以减少手写转换 Lambda。

Lambda

Lambda 是编排中的通用胶水:

  • 修剪和规范化输入;
  • 组装 Prompt 变量;
  • 将 Document 格式化为 context;
  • 解析模型 JSON;
  • 执行业务校验;
  • 计算路由标签;
  • 调用已有领域服务;
  • 合并或转换流。

Lambda 的函数保持单一职责。领域规则放进独立 package,编排图保留“这一步做什么”的语义。

原生流式处理

Compile 后的 Runnable 统一提供四种调用范式:

方法输入输出
Invoke完整 I完整 O
Stream完整 IStreamReader[O]
CollectStreamReader[I]完整 O
TransformStreamReader[I]StreamReader[O]

组件只需实现符合业务语义的方式。例如 ChatModel 实现 Generate 和 Stream,Retriever 通常只提供非流式 Retrieve。编排引擎自动处理常见转换:

能力作用
Auto Box将完整值包装成单 chunk 流
Auto Concat将多个 chunk 拼成完整值
Auto Copy将同一条流复制给多个下游或 Callback
Auto Merge合并多个上游流

示例链路:

Retriever.Invoke
  ↓ 完整 Documents
FormatDocs.Invoke
  ↓ 完整 context
ChatTemplate.Invoke
  ↓ 完整 Messages
ChatModel.Stream
  ↓ Message chunks
SSE.Transform
  ↓ HTTP chunks

如果流式模型后面连接一个只接受完整消息的节点,框架会先执行 Concat。该节点之后才能继续运行,首帧延迟优势会在此处结束。设计流式链路时要标出所有 Collect/Concat 边界。

自定义流类型需要注册或实现正确的拼接规则,并测试空流、单 chunk、多 chunk、错误和提前关闭。

Callback 与切面机制

Callback 是统一的横切切点,支持:

  • OnStart
  • OnEnd
  • OnError
  • OnStartWithStreamInput
  • OnEndWithStreamOutput

运行时注入:

result, err := app.Invoke(
	ctx,
	input,
	compose.WithCallbacks(handler),
)

不同节点可观察的信息:

节点典型字段
ChatModel模型、token、首帧延迟、结束原因
Retrieverquery、TopK、filter、返回数量、score
Tooltool name、ToolCallID、参数摘要、错误
Embedding批大小、维度、模型、耗时
Lambda节点名、输入输出类型、耗时
Graph分支、步骤数、中断与结束状态

日志、指标和 Trace 适合用 Callback。限流、重试、failover、鉴权和输出改写可以通过组件包装、节点逻辑或 ADK Middleware 实现。不同切点共同组成完整的横向治理能力。

CallOption

CallOption 将请求级参数传递给整张图、某类组件或指定节点。常见场景包括:

  • 本次请求使用更低 temperature;
  • Retriever 临时调整 TopK;
  • 只给某个模型节点指定回调;
  • 给特定节点增加实现专属 Option。

CallOption 可以避免为每次参数变化重建整张图。节点覆盖与全局默认值的优先级要在项目中统一约定。

v0.9.12 的实际写法如下:

result, err := app.Invoke(
	ctx,
	input,
	// 所有 ChatModel 节点都能看到该 Option。
	compose.WithChatModelOption(
		model.WithTemperature(0.1),
	),
	// 只传给顶层 key 为 retrieve_policy 的 Retriever。
	compose.WithRetrieverOption(
		retriever.WithTopK(8),
	).DesignateNode("retrieve_policy"),
	// 定位到子图中的具体节点。
	compose.WithCallbacks(traceHandler).DesignateNodeWithPath(
		compose.NewNodePath("rag_subgraph", "rerank"),
	),
	// 覆盖本次有环图运行步数上限。
	compose.WithRuntimeMaxSteps(30),
)

DesignateNode 只定位顶层节点;嵌套图使用 DesignateNodeWithPath。组件 Option 的实际生效还取决于组件实现是否读取公共与实现专属 Option。

State、Interrupt 与 Checkpoint

Local State 是单次 Graph 运行期间共享的状态。推荐通过 StatePreHandler 和 StatePostHandler 在节点边界读写,让业务组件保持 state-agnostic。

并行分支修改同一状态字段时,需要定义写入顺序或合并策略。State 适合请求级中间数据,跨会话数据应放入持久化 Session 或业务存储。

需要等待人工审批或外部输入时:

节点请求外部输入
  ↓ Interrupt
保存拓扑位置和 State
  ↓ CheckpointStore
取得审批结果
  ↓ Resume
从中断位置继续

退款、删除和工单确认等流程可以借助 Interrupt/Resume 跨越请求与进程。业务动作还要使用幂等键,防止恢复或重试导致重复执行。

Chain、Graph、Workflow 和 Agent 的选择

控制方式使用场景
Chain固定 A → B → C
Graph分支、并行、循环和状态
Workflow多上游字段映射
Flow复用已经沉淀的场景模式
Lambda自定义 Go 逻辑
ADK模型自主决定下一步
Graph Tool将稳定复杂流程作为 Agent 的一项能力

确定性步骤优先交给编排,语义决策和动态行动交给 Agent。常见生产架构是外层 Agent 选择能力,内层 Graph/Workflow 可靠执行。

Flow 集成

Eino Flow 把大模型应用中反复出现的组合模式封装成可直接调用的实现。组件层解决单个能力的接口统一,Compose 解决任意节点的连接,Flow 在两者之上提供已经组装好的场景模板。

ChatModel / Tool / Retriever / Indexer / Transformer
                 Chain / Graph 编排
                    Flow 模式
       直接调用 / 作为组件 / 作为子图 / 包装为 Lambda

Flow 位于 github.com/cloudwego/eino/flow/...。课程把它放在编排层的上部:Flow 的内部通常包含一个或多个 Chain/Graph,并继续使用组件 Option、Callback、流和 State。它既可以独立运行,也可以进入更大的 Graph,之后还可以包装成 Tool 交给 ADK Agent 调用。

v0.9.12 提供的主要 Flow

模式解决的问题对外接口
flow/agent/reactReAct Agent模型与 Tool 之间循环,直到模型给出最终回答GenerateStreamExportGraph
flow/agent/multiagent/hostHost Multi-AgentHost 识别意图并路由到一个或多个 SpecialistGenerateStreamExportGraph
flow/retriever/multiqueryMultiQuery Retriever改写多个 query、并发召回、融合去重实现 retriever.Retriever
flow/retriever/routerRouter Retriever选择一组 Retriever、并发召回、RRF 或自定义融合实现 retriever.Retriever
flow/indexer/parentParent Indexer切分父文档、记录父子关系、索引子文档实现 indexer.Indexer
flow/retriever/parentParent Retriever先召回子文档,再取得完整父文档实现 retriever.Retriever

这些实现共享两个特征:

  1. 业务代码仍然面向 Eino 的标准组件接口;
  2. Flow 内部的模型、检索、融合和子图节点仍可进入 Callback 与 Trace。

Flow ReAct Agent

Flow ReAct Agent 的内部拓扑由 ChatModel、ToolsNode、State 和条件分支组成:

START
ChatModel ── 无 ToolCall ─────────────→ END
  └─ 有 ToolCall → ToolsNode
                     ├─ ReturnDirectly → END
                     └─ 普通结果 → ChatModel

每次进入 ChatModel 前,State 会累积输入消息、模型 ToolCall 和 Tool Result。模型不再产生 ToolCall 时,当前 assistant message 成为最终结果。

最小示例:

reactFlow, err := react.NewAgent(ctx, &react.AgentConfig{
	ToolCallingModel: cm,
	ToolsConfig: compose.ToolsNodeConfig{
		Tools:               tools,
		ExecuteSequentially: true,
	},
	MessageRewriter: func(
		ctx context.Context,
		messages []*schema.Message,
	) []*schema.Message {
		// 可以在这里压缩历史;返回值会写回 Flow State。
		return messages
	},
	MaxStep: 12,
	ToolReturnDirectly: map[string]struct{}{
		"finish_report": {},
	},
})
if err != nil {
	return err
}

answer, err := reactFlow.Generate(ctx, []*schema.Message{
	schema.SystemMessage("你是企业客服。业务数据必须来自工具。"),
	schema.UserMessage("查询订单 ORD-1001 的物流"),
})
if err != nil {
	return err
}
fmt.Println(answer.Content)

关键配置:

配置v0.9.12 语义
ToolCallingModel推荐入口,要求实现并发安全的 WithTools;旧 Model model.ChatModel 字段已弃用
ToolsConfig传给 ToolsNode,包含工具、执行顺序、未知工具处理和 Tool Middleware
MessageModifier每次模型调用前处理消息副本,修改不会写回 State
MessageRewriter修改累积消息并写回 State,适合历史压缩和持久改写;它先于 MessageModifier 执行
MaxStepGraph 最大运行步数;值为 0 时使用“内部节点数 + 10”,当前两节点拓扑的默认值为 12;一次 ReAct 迭代通常经过模型与工具两个节点
ToolReturnDirectly指定静态直返工具;命中的 Tool Result 直接成为 Flow 输出;同轮命中多个静态直返工具时返回第一个
StreamToolCallChecker判断流式模型输出是否包含 ToolCall

Tool 还可以在执行期间调用 react.SetReturnDirectly(ctx) 动态请求直返。动态设置优先于 ToolReturnDirectly,适合工具根据业务结果决定是否还需要模型加工。同轮多个 Tool 都动态设置时,最后一次调用对应的 ToolCall 生效。

流式模式需要特别测试。默认 StreamToolCallChecker 跳过开头的空 chunk,然后检查第一个非空 chunk;某些模型会先输出文本,随后才输出 ToolCall,此时需要自定义 checker。自定义 checker 必须关闭传入的 StreamReader

调用时的模型和工具 Option 通过 Flow 适配器传入:

answer, err := reactFlow.Generate(
	ctx,
	messages,
	react.WithChatModelOptions(model.WithTemperature(0.1)),
)

react.WithToolOptions(...) 可以继续传入具体 Tool 实现定义的 tool.Option。调用级 Option 要与实现能力配套,并通过集成测试确认生效。

运行时动态换工具时使用 react.WithTools(ctx, tools...),它会同时更新模型看到的 ToolInfo 与 ToolsNode 的可执行列表。react.WithToolList(...) 只更新 ToolsNode,在 v0.9.12 已标记 Deprecated,单独使用会造成模型工具定义和执行列表不一致。

MultiQuery Retriever

MultiQuery Retriever 先生成多个查询表达,再并发调用同一个底层 Retriever,最后融合各路结果:

原始 query
   ↓ RewriteLLM 或 RewriteHandler
query_1  query_2  query_3
   ↓        ↓        ↓
并发调用 OrigRetriever
   └────────┼────────┘
      FusionFunc / 按 ID 去重
multiRetriever, err := multiquery.NewRetriever(
	ctx,
	&multiquery.Config{
		RewriteLLM:    rewriteModel,
		OrigRetriever: baseRetriever,
		MaxQueriesNum: 3,
		// RewriteTemplate、LLMOutputParser、FusionFunc 为空时使用默认实现。
	},
)
if err != nil {
	return err
}

docs, err := multiRetriever.Retrieve(ctx, "耳机拆封后能否七天无理由退货")

默认模板要求模型按行返回查询,默认 parser 按换行切分,MaxQueriesNum 默认 5,默认 FusionFunc 按 Document.ID 去重并保留首次出现顺序。生产项目通常还要:

  • 清理空行、编号和重复 query;
  • 保留或显式补入原始 query;
  • 限制生成数量与总检索并发;
  • 为每路召回记录 query、耗时、错误和文档 ID;
  • 使用 RRF、加权融合或 reranker 取代简单去重;
  • 评估 query rewrite 对 recall、延迟和 token 的实际影响。

MaxQueriesNum 使用 0 时应用默认值 5,业务配置应拒绝负数;源码对负数没有校验,生成结果切片时会触发 panic。

课程锁定的 v0.9.12 以及审计时的 main@922b6a8 中,MultiQuery Retriever 的 Retrieve(...opts) 都没有把调用级 retriever.Option 转发给 OrigRetriever。TopK、filter、tenant 和 ACL 等安全条件应固化在底层 Retriever 实例或请求上下文中,并编写集成测试;升级版本后重新核对 Option 传递行为。

Router Retriever

Router Retriever 根据 query 选择一个或多个已注册 Retriever,选中的 Retriever 并发执行。调用级 retriever.Option 会转发到每个被选中的 Retriever。默认 FusionFunc 使用 RRF,源码用从 0 开始的结果下标计算 1 / (index + 60) 并求和,再按该值降序排列。

routedRetriever, err := router.NewRetriever(ctx, &router.Config{
	Retrievers: map[string]retriever.Retriever{
		"policy":  policyRetriever,
		"product": productRetriever,
		"order":   orderRetriever,
	},
	Router: func(ctx context.Context, query string) ([]string, error) {
		switch {
		case strings.Contains(query, "订单"):
			return []string{"order"}, nil
		case strings.Contains(query, "退货"):
			return []string{"policy", "product"}, nil
		default:
			return []string{"policy", "product"}, nil
		}
	},
})

Router 返回的名称必须已注册,空结果会返回错误。默认 RRF 只使用融合值排序,没有把融合值写回 Document.Score();需要展示融合分数时使用自定义 FusionFunc 或另存元数据。

v0.9.12 构造代码会准备“Router 为空时选择全部 Retriever”的默认函数,最终对象却保存了 config.Router,调用时会产生 nil function panic。审计时的 main@922b6a8 仍保留该行为。课程示例始终显式提供 Router,并在配置校验阶段禁止 nil。

Parent Indexer 与 Parent Retriever

父子文档模式兼顾小 chunk 的定位能力和大文档的完整语境:

入库:父文档 → Transformer → 子文档
                      ├─ Metadata[parent_id] = 父文档 ID
                      ├─ 生成唯一子文档 ID
                      └─ 写入底层 Indexer

查询:query → 召回子文档 → 提取并去重 parent_id
                     OrigDocGetter(ids)
                         完整父文档

Parent Indexer 的配置包括底层 Indexer、Transformer、ParentIDKeySubIDGenerator。Transformer 输出需要满足以下契约:

  • 每个子文档切分后仍以父文档 ID 作为初始 Document.ID
  • 同一父文档的所有子文档连续排列;
  • SubIDGenerator 返回数量与该父文档的 chunk 数完全一致;
  • ParentIDKey 使用稳定且非空的元数据 key。

Parent Indexer 只把子文档交给底层 Indexer。完整父文档需要由业务应用另外保存;Parent Retriever 通过 OrigDocGetter(ctx, ids) 从文档库取得它们。元数据值只有 string 类型才会进入去重结果,源码没有过滤空字符串,入库校验应保证 parent ID 非空。父文档的最终顺序和内容完全由 OrigDocGetter 返回值决定,若要保留子文档召回顺序,Getter 必须按输入 ID 重排。权限控制要同时覆盖子文档召回和父文档读取,避免合法命中的 child ID 被用来读取越权 parent。

Flow 进入 Chain 或 Graph 的三种方式

官方 Flow 文档给出三种组合方式,优先级取决于 Flow 对外暴露的能力。

第一种,作为标准组件加入。MultiQuery、Router、Parent Retriever 都实现了 retriever.Retriever

chain := compose.NewChain[string, []*schema.Document]()
chain.AppendRetriever(multiRetriever)

app, err := chain.Compile(ctx)
if err != nil {
	return err
}
docs, err := app.Invoke(ctx, query)

第二种,导出内部 Graph 作为子图。Flow ReAct Agent 和 Host Multi-Agent 都提供 ExportGraph

reactGraph, graphOpts := reactFlow.ExportGraph()
graphOpts = append(
	graphOpts,
	compose.WithNodeKey("customer_service_react"),
)

outer := compose.NewChain[[]*schema.Message, *schema.Message]()
outer.AppendGraph(reactGraph, graphOpts...)

app, err := outer.Compile(ctx)

导出 Graph 可以保留内部节点结构,外层编排能够按 NodePath 下发 Option、Callback 和调试配置,也能在 Eino Dev 中观察嵌套拓扑。

第三种,把 GenerateStream 或普通调用函数包装为 Lambda。该方式适用于没有暴露组件接口或 Graph 的自定义 Flow;运行时只看到 Lambda 边界,内部节点级 Option 和拓扑信息需要额外适配。

接入方式适用条件主要特点
作为组件Flow 实现某个组件接口调用简单,组件语义和 Option 保持统一
作为子图Flow 可完整导出 Graph保留内部节点、嵌套路径和编译信息
作为 Lambda任意可调用 Flow通用,外层只感知输入和输出

Host Multi-Agent

Host Multi-Agent 将每个 Specialist 的 NameIntendedUse 转成 ToolInfo,Host 模型通过 ToolCall 选择 Specialist。Host 只执行意图识别和转发,不生成新的子任务描述。

                    ┌─ 无 ToolCall → Host 直接回答
用户消息 → Host ────┤
                    ├─ 一个 Specialist → 直接返回专家结果
                    └─ 多个 Specialists → 并行执行 → Summarizer

Specialist 可以由 ChatModel 实现,也可以提供 InvokableStreamable,因此可接入 Chain、Graph 或 Flow ReAct Agent。多个 Specialist 被选中时,框架使用配置的 Summarizer;缺省 Summarizer 只拼接结果,并且不支持流式汇总。

Host 推荐配置 ToolCallingModel,旧 ChatModel 字段在 v0.9.12 已弃用。流式 ToolCall 检测与 Flow ReAct 使用相同的“首个非空 chunk”默认策略,接入先文本后 ToolCall 的模型时需要自定义 StreamToolCallChecker

Flow Agent 与 ADK 的选择

Flow Agent 和 ADK 都提供 ReAct 与多 Agent 能力,两者的运行接口和扩展面不同:

维度Flow AgentEino ADK
主要入口flow/agent/reactflow/agent/multiagent/hostadk.ChatModelAgent、Runner、预构建 Agent
输出最终 Message 或 MessageStreamAsyncIterator 中的 AgentEvent
编排复用ExportGraph 后作为子图AgentTool、Graph Tool、Runner 和 ADK 协作模式
运行时扩展Compose Option、Callback、State、Graph 节点Handler、SessionValues、事件、Cancel、Interrupt/Resume、Checkpoint、TurnLoop
消息类型*schema.Message*schema.Message;AgenticMessage 能力边界见 11.2.10
适合场景复用已有 Flow、直接嵌入 Graph、标准 RAG 模式需要统一 Agent Runtime、事件消费、状态治理和 Agent 协作

v0.9.12 源码没有把 flow/agent/reactflow/agent/multiagent/host 标记为 Deprecated。v0.9 Migration Notes 中的 NOT RECOMMENDED 范围指向 ADK 的 Agent Transfer、Workflow Agent 和 Supervisor API,没有包含这两个 Flow package。

课程项目使用 ADK ChatModelAgent,因为它需要 Runner、AgentEvent、Middleware 和后续的 Interrupt/Resume 扩展。已有 Flow ReAct 项目可以继续维护;新的 RAG 组合优先评估 MultiQuery、Router、Parent 等标准 Flow;需要把确定性 Flow 暴露给 Agent 时,可以将编译后的 Graph 包装成 Tool。

11.4 ADK(Agent Development Kit)

ADK 面向自主决策类任务,基于组件层和编排层构建。它把常见的 Agent 执行范式、工具循环、事件流、状态和协作方式封装成可直接使用的能力。

ChatModel + Tool + ToolsNode
编排循环、状态、流、Callback
ChatModelAgent + Runner + AgentEvent

Agent 抽象

type TypedAgent[M MessageType] interface {
	Name(ctx context.Context) string
	Description(ctx context.Context) string
	Run(ctx context.Context, input *TypedAgentInput[M], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]]
}

type Agent = TypedAgent[*schema.Message]

Run 启动执行后立即返回 AsyncIterator,模型输出、Tool 调用、子 Agent 事件、中断和错误通过 AgentEvent 持续交付。v0.9 的 MessageType 支持 *schema.Message*schema.AgenticMessage

单 Agent:ChatModelAgent 与 ReAct

ChatModelAgent
  = Model
  + Instruction
  + Tools
  + ReAct Loop
  + Middleware

创建 Agent:

customerService, err := adk.NewChatModelAgent(
	ctx,
	&adk.ChatModelAgentConfig{
		Name:        "customer_service",
		Description: "处理政策、订单、物流与工单问题。",
		Instruction: "你是企业客服。政策问题先检索知识库;" +
			"订单与物流数据必须来自工具;" +
			"创建工单前必须获得明确确认;" +
			"资料不足时追问,禁止编造。",
		Model:         cm,
		MaxIterations: 12,
		ToolsConfig: adk.ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools:               tools,
				ExecuteSequentially: true,
			},
		},
	},
)

ReAct 循环:

Reason:模型阅读目标、历史、观察和工具定义
Action:模型生成 ToolCall
Act:ToolsNode 执行 Tool
Observation:Tool Result 加入消息
模型继续 Reason
没有 ToolCall 时输出最终回答

没有配置 Tool 时,ChatModelAgent 执行一次模型调用。MaxIterations 默认值为 20,达到上限会返回错误。生产环境要记录迭代次数、每个 Tool 的调用次数、最后一轮模型输出和退出原因。

Instruction 在默认 GenModelInput 中按 FString 语法使用 SessionValues 渲染,例如 {tenant_name}。Instruction 中需要保留 JSON 花括号,或 SessionValues 只用于业务上下文时,应提供自定义 GenModelInput,避免模板解析改变内容。

ModelRetryConfigModelFailoverConfig 可以在 ChatModelAgent 内配置模型重试和故障切换。重试条件、退避、最大次数以及可切换模型由应用明确提供;带副作用的 Tool 不会因此自动获得幂等语义。

ToolsConfig 配置作用
ToolsNodeConfigTool、执行顺序、未知工具处理
ReturnDirectly某个 Tool 完成后直接结束
EmitInternalEvents透传 AgentTool 内部事件

EmitInternalEvents 只把子 Agent 事件转发给外部事件消费者,这些事件不会写入父 Agent 的 run session,也不会影响父 Agent 的 state 或 checkpoint。子 Agent 的最终 Tool Result 仍会按正常工具结果进入父 Agent 消息轨迹。

Runner 与事件流

runner := adk.NewRunner(ctx, adk.RunnerConfig{
	Agent:           customerService,
	EnableStreaming: true,
	CheckPointStore: checkpointStore,
})

iter := runner.Query(
	ctx,
	"查询订单 ORD-1001 的物流",
)

for {
	event, ok := iter.Next()
	if !ok {
		break
	}
	if event.Err != nil {
		return event.Err
	}
	consume(event)
}

传入完整历史和 SessionValues:

iter := runner.Run(
	ctx,
	append(history, schema.UserMessage(question)),
	adk.WithSessionValues(map[string]any{
		"customer_id": "CUST-001",
		"tenant_id":   "TENANT-A",
	}),
)
AgentEvent 字段含义
AgentName产生事件的 Agent
RunPath框架维护的执行路径;主要服务 Agent Transfer 与旧 Workflow Agent,AgentTool/DeepAgent 场景通常很短
OutputMessageOutput 或应用定义的 CustomizedOutput;MessageOutput 可携带完整 Message 或 MessageStream
ActionExit、Interrupted、Transfer、BreakLoop 或自定义动作
Err执行错误

MessageOutput 可能包含 MessageStream。应用读取 chunk 时还要合并完整消息,用于历史、审计和下一轮输入。

多轮历史要保存用户消息、assistant ToolCall、tool result 和最终 assistant 消息。丢失工具调用轨迹会影响下一轮指代理解,也可能导致模型重复调用。

状态管理

状态生命周期典型内容
Agent Run State一次执行当前迭代、消息、工具中间结果
SessionValues一段会话user、tenant、偏好、业务上下文
Conversation History多轮对话完整 Message 序列
Long-term Memory跨会话已确认事实、长期偏好、摘要
Checkpoint中断到恢复运行位置、State、待处理 Action

v0.9.12 的 ChatModelAgentState 直接保存 MessagesToolInfosDeferredToolInfos;Handler 还可以通过 RunLocalValue API 保存一次 Run 内且可随 checkpoint 序列化的自定义值。SessionValues 由 Runner 注入。Conversation History 与跨会话 Long-term Memory 由应用存储和回填,当前稳定源码没有通用 Long-term Memory Store 接口。

Session、History 和 Long-term Memory 都要按 user/tenant 隔离,并设置过期、容量和删除策略。长期记忆写入前需要验证,避免把模型推测持久化成用户事实。

普通 HTTP 一问一答使用 Runner。长任务、持续输入和抢占可以使用 TurnLoop:

  • Run 非阻塞启动后台循环,Wait 等待退出并取得 TurnLoopExitState
  • Push 提交新输入并返回是否入队成功;
  • Push(item, adk.WithPreempt(...))WithPreemptTimeout(...) 在入队的同时请求抢占当前 turn;
  • Stop 默认等待当前 turn 完成,也可选择立即、优雅或超时升级的取消策略;
  • 在同一生命周期内处理多轮事件。

工具自动调度

ADK 自动完成 ToolInfo 收集、ToolCall 解析、ToolsNode 调用、Tool Result 回填和 ReAct 循环。业务应用继续负责:

  • Tool 的真实实现;
  • 鉴权、租户隔离和数据范围;
  • 参数业务校验;
  • 超时、限流、熔断与审计;
  • 副作用确认和幂等;
  • 失败分类和用户可理解的降级路径。

工具数量较多时,可以使用 adk/middlewares/dynamictool/toolsearch 动态暴露当前轮需要的工具,降低上下文消耗和误选概率。UseModelToolSearch=false 由本地工具完成搜索,true 使用 provider 原生 Tool Search;后一种模式需要所选模型组件与 provider 共同支持。

多 Agent 协同

模式实现思路场景
主从模式主 Agent 将子 Agent 包装为 Tool专业任务委派
扁平协作Graph 并行运行多个 Agent 后汇总多视角分析
顺序协作一个 Agent 的输出交给下一个起草、审核、修订
循环协作生成 Agent 与评审 Agent 迭代Evaluator-Optimizer

当前稳定版推荐 AgentAsTool:

researcher, err := adk.NewChatModelAgent(
	ctx,
	&adk.ChatModelAgentConfig{
		Name:        "researcher",
		Description: "搜索资料并返回带来源的摘要。",
		Instruction: "只返回结论、证据与来源。",
		Model:       cm,
		ToolsConfig: adk.ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{searchTool},
			},
		},
	},
)
if err != nil {
	return err
}

researchTool := adk.NewAgentTool(ctx, researcher)

NewAgentTool 在调用 Info 时要求子 Agent 的 Name 与 Description 都非空。默认参数 Schema 只接收一段独立的 request,子 Agent 不继承父 Agent 的完整历史;确有需要时可以显式使用 adk.WithFullChatHistoryAsInput()。多个 AgentTool 可以由 ToolsNode 并发调用,内部事件支持嵌套观测。

子 Agent 的 Interrupted Action 会穿过 AgentTool 边界,以支持嵌套审批与恢复;Exit、TransferToAgent 和 BreakLoop 只影响子 Agent 本次执行。这个作用域可以防止子 Agent 终止父 Agent 的整体运行。

v0.9 Migration Notes 将全量上下文共享的 Agent Transfer、旧 Supervisor 和旧 Workflow Agent API 标记为 NOT RECOMMENDED。新项目优先使用 ChatModelAgent + AgentTool、DeepAgents,以及 Graph/Workflow 的确定性协作。

扁平协作需要明确汇总器、冲突处理、超时和终止条件。每个 Agent 只接收完成职责所需的最小上下文。

Middleware

v0.9.12 同时保留两套扩展入口。ChatModelAgentConfig.Middlewares []AgentMiddleware 已标记 Deprecated;新代码使用 Handlers []TypedChatModelAgentMiddleware[M]。默认消息类型下可直接写 Handlers []adk.ChatModelAgentMiddleware

Handler 点位常见用途
BeforeAgent注入 Instruction 和运行上下文
BeforeModelRewriteState压缩历史;读写 state.ToolInfos,动态筛选工具
AfterModelRewriteState校验模型结果并持久修改后的 state
WrapModel重试、超时、failover
WrapInvokableToolCall / WrapStreamableToolCall包装普通 Tool 的同步或流式调用
WrapEnhancedInvokableToolCall / WrapEnhancedStreamableToolCall包装多模态 Enhanced Tool 调用
AfterAgent清理和结束统计

实现自定义 Handler 时嵌入 *adk.BaseChatModelAgentMiddleware,它为所有方法提供空操作实现,业务代码只覆盖需要的点位:

type AuditHandler struct {
	*adk.BaseChatModelAgentMiddleware
}

func (h *AuditHandler) BeforeModelRewriteState(
	ctx context.Context,
	state *adk.ChatModelAgentState,
	_ *adk.ModelContext,
) (context.Context, *adk.ChatModelAgentState, error) {
	// 可在这里压缩 state.Messages,或依据权限筛选 state.ToolInfos。
	// 修改后的 state 会用于本轮模型调用,并保留到后续迭代。
	return ctx, state, nil
}

audit := &AuditHandler{
	BaseChatModelAgentMiddleware: &adk.BaseChatModelAgentMiddleware{},
}

agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
	Name:     "customer_service",
	Model:    cm,
	Handlers: []adk.ChatModelAgentMiddleware{audit},
})

多个 Handler 的 Wrapper 方法按注册顺序组成包装链,第一个注册项位于最外层,例如 [A, B, C] 形成 A(B(C(target)))。Before/After Hook 都按注册顺序调用并采用 fail-fast;AfterAgent 只在最终回答或 return-directly 成功结束时执行,错误、取消与迭代超限不会触发它。旧 Middlewares 先处理,Handlers 随后处理。Hook 顺序、错误短路、流式错误和工具包装都要写组合测试。Callback 适合观察执行,Handler 可以修改 context、state、模型和工具调用行为。

Interrupt、Checkpoint 与 Resume

Agent 准备执行高风险 Tool
Tool、Graph 节点或自定义 Agent 产生 Interrupt
Runner 将状态写入 CheckPointStore
外部审批系统取得用户决定
Resume 携带审批结果
Agent 从中断点继续

自定义 Agent 可以返回 adk.Interrupt(ctx, info)adk.StatefulInterrupt(ctx, info, state) 事件;Tool 与 Graph 节点使用 Compose 的 Interrupt API。Runner 配置 CheckPointStore,运行时通过 adk.WithCheckPointID(id) 指定持久化 key。恢复分为两种:

  • runner.Resume(ctx, checkpointID) 隐式恢复全部中断点;
  • runner.ResumeWithParams(...)InterruptCtx.ID 提供目标数据,适合嵌套或并行中断。

Checkpoint 使用 gob 序列化,自定义 state 类型需要注册并保持版本兼容。跨进程或长时间审批还要让 approval ID、session ID、run ID、checkpoint ID 和业务幂等键可以关联。

ADK 的使用边界

ADK 适合步骤随观察动态变化、Tool 选择依赖自然语言、需要多步补参或子任务委派的场景。Graph/Workflow 适合业务规则确定步骤、要求强可预测性、涉及事务补偿和严格合规的流程。

11.5 eino-ext 与 DevOps 层

EinoExt 包含组件的具体实现、Callback handler、DevOps 调试服务和其他扩展。核心接口与基础设施实现分离后,应用可以只引入需要的 module。

组件的具体实现

组件类型EinoExt 中的示例
ChatModelOpenAI、Ark、Claude、Gemini、DeepSeek、Ollama、Qwen
AgenticModelAgentic OpenAI、Ark、Claude、DeepSeek、Gemini、Qwen
EmbeddingOpenAI、Ark、Gemini、Ollama、DashScope
Loader / ParserFile、URL、S3 与 PDF、DOCX、HTML、XLSX Parser
TransformerRecursive、Markdown、HTML、Semantic Splitter 与 Score Reranker
IndexerMilvus、Elasticsearch、OpenSearch、Qdrant、Redis
RetrieverMilvus、Elasticsearch、OpenSearch、Qdrant、Redis、Dify
ToolMCP、搜索、HTTP、命令行、Wikipedia
PromptCozeLoop、MCP 等远程 Prompt

EinoExt 是多 module 仓库。依赖管理应锁定每个子 module 版本,核对它支持的 Eino 核心版本,并为供应商差异编写集成测试。

DevOps 层定位

DevOps 层贯穿组件、编排和 Agent,负责可观测、调试、评估与运维。它不决定业务路由,也不执行订单、物流和工单规则。

Components / Graph / ADK
  ├─ Callback → Trace / Metrics / Logs
  ├─ Debug protocol → Eino Dev
  ├─ Run result → Evaluation
  └─ Prompt version → Prompt Platform

链路追踪

Eino Callback 可以观察 ChatModel、Embedding、Retriever、Tool、Prompt、Lambda 与 Graph。审计时的 EinoExt main 明确包含以下 Callback module:

module作用
callbacks/langfuse实现 callbacks.Handler,上报 trace、span、generation 与流式事件
callbacks/langsmith接入 LangSmith
callbacks/cozeloop接入 CozeLoop
callbacks/apmplus使用 OpenTelemetry trace/metric 接入 APMPlus

libs/acl/opentelemetry 提供 OTLP exporter、TracerProvider 和 MeterProvider 的初始化封装。当前源码树没有通用的 callbacks/opentelemetry module;应用需要选择现有平台 Handler,或实现 Callback Handler 把 Eino 事件转换成 span。OTLP 数据可以发送到 Jaeger 等兼容后端。

一次客服运行的 Trace:

run customer_service
├─ model decide
│  ├─ input/output token
│  └─ time_to_first_token
├─ tool search_knowledge
│  ├─ tenant/filter/top_k
│  └─ returned_documents
├─ model answer
└─ ticket confirmation

推荐记录 trace ID、run ID、session ID、模型、Prompt 和工具版本、token、首帧延迟、ToolCallID、Retriever filter、分支、循环次数、错误、重试与降级。

Prompt、回答、Tool 参数和检索正文可能包含敏感信息。默认记录摘要、长度、哈希和业务 ID,对手机号、地址、订单和身份字段脱敏,并设置访问控制、采样率和保留期。

可视化调试

Eino Dev 包含 IDE 插件和应用进程内的调试服务:

go get github.com/cloudwego/eino-ext/devops@v0.1.9
if err := devops.Init(ctx); err != nil {
	return err
}

// devops.Init 在 Compile 之前调用。
app, err := graph.Compile(ctx)

调试插件可以发现已 Compile 的 Chain、Graph 与 Workflow,从 START 或中间节点发起测试,根据类型生成 mock 输入,并查看节点输入、输出、错误、分支和拓扑结构。

课程将 DevOps module 锁定到 v0.1.9,保证示例可复现;官方快速开始中的 @latest 会随发布时间变化。官方文档说明 v0.1.9 起调试服务默认监听 127.0.0.1,默认端口为 52538。远程调试需要显式监听可访问地址,并补充认证、网络隔离和防火墙。

源码审计发现 devops/v0.1.9 在写入调试指标时把 CompletionTokens 误赋值为 PromptTokens。EinoExt main@550170e 已改为读取真实的 TokenUsage.CompletionTokens,审计时还没有更新的 DevOps 稳定 Tag。使用 v0.1.9 时,节点输入输出、拓扑与耗时仍可用于调试;completion token 指标需要从模型 Callback、provider usage 或应用日志交叉核对。

评估体系

框架提供执行、Callback 和可回放数据,评估平台负责数据集、判官、报表与实验管理。审计时的 Eino 核心与 EinoExt 源码树中没有独立的 evaluation package,下面的评估流程属于基于 Eino 运行数据搭建的方法,LLM-as-Judge、数据集管理和实验对比由外部平台或项目代码实现。

层级方法示例
确定性评估单测、规则、Schema引用存在、参数合法、权限正确
回答评估LLM-as-Judge正确性、相关性、完整性、语气
轨迹评估规则或 Agent-as-a-JudgeTool 选择、顺序、迭代、越权

标准流程:

  1. 建立输入、期望结果和标签组成的数据集;
  2. 固定代码、模型、Prompt、Tool 和知识库版本;
  3. 批量执行并保存完整 Trace;
  4. 计算成功率、引用正确率、延迟与成本;
  5. 用 LLM-as-Judge 评估开放式回答;
  6. 用人工样本校准判官;
  7. 比较基线与候选版本;
  8. 把线上失败样本补充进回归集。

客服常用指标包括 groundedness、引用正确率、Tool 选择准确率、参数一次生成成功率、未经确认执行副作用的次数、转人工召回率、P50/P95 延迟和单次成本。

提示词管理

EinoExt 当前提供 components/prompt/cozeloopcomponents/prompt/mcp 等远程 Prompt 适配,企业也可以实现 prompt.ChatTemplate 接口接入内部系统。版本化、在线调试、灰度发布、审批与回滚由所接入的平台提供,Eino 的 Prompt 接口负责在运行时取得并格式化消息。

Prompt 运维需要:

  • 不可变版本;
  • 开发、预发和生产环境;
  • 模型、Tool Schema、知识库版本关联;
  • 在线调试与离线评估;
  • 灰度实验与快速回滚;
  • 变更审批和审计;
  • Trace 中记录实际 Prompt 版本。

灰度时可以基于 user/session 稳定哈希选择版本,评估结果按 Prompt 版本分组。

能力边界

开源 Eino/EinoExt 可以直接提供组件接口与实现、Callback 适配、调试服务和运行数据。统一评估工作台、Prompt 灰度、线上告警与权限治理通常由 CozeLoop、Langfuse、LangSmith、APMPlus 或企业内部平台提供。技术选型要分别核对核心接口、适配器与平台功能。

11.6 项目实战:从 0 到 1 实现智能客服

本节使用配套项目 code/11-customer-service-assistant/ 构建企业智能客服。项目将 Eino Agent、业务 Tool、Milvus 混合检索、引用、租户隔离和工单确认连成完整流程。

学习 Eino API 时可以先运行 code/11-eino-assistant/。这个基础项目只有 calc、now 两个 Tool,重点是 ChatModelAgent、Runner、AgentEvent 和 history。

需求拆解

智能客服需要处理四类问题:

类型处理方式数据来源
政策和产品问题RAG 检索后回答企业知识库
订单问题调用 query_order订单服务
物流问题调用 query_logistics物流服务
人工升级草稿、确认、创建工单工单服务

业务约束:

  • 政策回答附带真实引用;
  • 订单和物流字段来自 Tool;
  • 客户只能查询自己的订单;
  • 每次知识检索带 tenant filter;
  • 空召回、资料冲突和缺少参数时追问或转人工;
  • 创建工单必须经过明确确认;
  • 模型、检索或 Tool 失败时提供稳定降级结果。

总体架构

客户输入
Runner
ChatModelAgent
  ├─ search_knowledge
  │    └─ Milvus Hybrid Search
  │         ├─ dense vector search
  │         ├─ BM25 sparse search
  │         ├─ tenant filter
  │         └─ RRF + lexical rerank
  ├─ query_order
  ├─ query_logistics
  ├─ draft_ticket
  └─ confirm_ticket
AgentEvent / MessageStream
当前 CLI;可扩展为 HTTP/SSE

四层能力在当前项目中的实际状态:

层级当前项目使用的实现状态
组件层Eino OpenAI ChatModel、utils.InferTool已接入 Eino 组件接口
编排层普通 Go 函数组合 Markdown 加载、切块、混合检索、重排与引用尚未接入 Eino Chain、Graph 或 Workflow;本节后半给出迁移方向
Agent 层ChatModelAgentRunnerAgentEvent、history已接入 ADK
DevOps 层debug event 与标准日志当前仅有基础观测;Eino Dev、Callback handler 与 OpenTelemetry 属于扩展任务

项目的 internal/embeddinginternal/knowledgeinternal/rag 是为教学场景实现的业务基础设施包,没有实现 Eino 的 document.Loaderdocument.Transformerembedding.Embedderindexer.Indexerretriever.Retriever 接口。学习者可以先理解完整 RAG 数据流,再按“将项目升级为完整 Eino 编排”一节迁移。

项目目录

code/11-customer-service-assistant/
├── cmd/
│   ├── assistant/       # 多轮客服
│   ├── indexer/         # 知识入库
│   └── search/          # 独立测试检索
├── fixtures/
│   ├── orders.json
│   └── shipments.json
├── internal/
│   ├── agent/           # Agent 和事件消费
│   ├── business/        # 订单、物流、工单领域逻辑
│   ├── config/          # 环境配置
│   ├── embedding/       # hash 与 OpenAI-compatible embedding
│   ├── knowledge/       # Markdown 加载和切块
│   ├── rag/             # Milvus 入库与混合检索
│   └── tools/           # Eino Tool 适配
├── knowledge/           # 政策和产品知识
└── docker-compose.yml   # Milvus Standalone

推荐阅读顺序:

  1. internal/config
  2. internal/knowledge
  3. internal/embedding
  4. internal/rag
  5. internal/business
  6. internal/tools
  7. internal/agent
  8. cmd

第一步:定义配置

应用配置包括:

  • 模型 API Key、BaseURL 和 Model;
  • embedding provider、模型和维度;
  • Milvus 地址、数据库和 collection;
  • tenant ID;
  • fixtures 与知识目录。

敏感配置从环境变量读取,默认值只用于本地开发。当前 config.Load 解析环境变量,并校验 embedding 维度至少为 32;agent.NewRunner 创建模型前校验 OPENAI_API_KEYknowledge.LoadDirbusiness.LoadRepository 在实际读取时检查目录内容和 fixture 文件。生产服务可以在统一启动检查阶段提前完成全部依赖校验。

第二步:加载与切分知识

知识目录包含退货、保修、物流、产品 FAQ 和工单 SLA。项目自定义的 knowledge.LoadDir 按 Markdown 文件读取,包内切分逻辑按二级标题生成 chunk。这两个步骤目前是普通 Go 函数;若迁移到 Eino 组件体系,可分别实现 document.Loaderdocument.Transformer

每个 chunk:

type Chunk struct {
	ChunkID string
	DocID   string
	Title   string
	Source  string
	Content string
}

稳定 ID 基于 source、标题和 chunk 内容生成。生产版本还要加入文档版本、部门、ACL、状态和生效时间。

入库流水线:

Markdown
  → Load
  → 按标题切块
  → 清洗空白
  → 生成稳定 ID
  → dense embedding
  → Milvus Upsert
  → BM25 Function 生成 sparse vector

第三步:选择 Embedding

项目支持两种实现:

Provider用途特点
hash本地教学零密钥、确定性、语义能力有限
OpenAI-compatible实际语义检索需要外部 API

hash embedding 根据中文字符 unigram、bigram 和词项 hashing 生成归一化向量。它适合验证数据链和单元测试,BM25 会承担更多召回。

接入真实 embedding:

export EMBEDDING_PROVIDER=openai
export EMBEDDING_API_KEY='your-api-key'
export EMBEDDING_BASE_URL='https://api.openai.com/v1'
export EMBEDDING_MODEL='text-embedding-3-small'
export EMBEDDING_DIMENSION=1536

provider、模型、维度或预处理变化后,使用新 collection 或执行 --recreate

第四步:设计 Milvus Collection

字段类型作用
chunk_idVarChar primary key稳定主键
tenant_idVarChar租户过滤
doc_idVarChar父文档 ID
titleVarChar引用标题
sourceVarChar来源文件
contentVarChar正文与 BM25 输入
dense_vectorFloatVectordense 检索
sparse_vectorSparseVectorBM25 检索

中文 content 开启 Jieba analyzer:

analyzer := map[string]any{
	"tokenizer": "jieba",
}

contentField := entity.NewField().
	WithName("content").
	WithDataType(entity.FieldTypeVarChar).
	WithMaxLength(8192).
	WithEnableAnalyzer(true).
	WithAnalyzerParams(analyzer).
	WithEnableMatch(true)

BM25 Function:

bm25 := entity.NewFunction().
	WithName("content_bm25").
	WithType(entity.FunctionTypeBM25).
	WithInputFields("content").
	WithOutputFields("sparse_vector")

chunk 主键将 tenant ID 纳入哈希,避免两个租户导入相同文档时覆盖。租户隔离还要在每次查询 filter 中强制执行。

第五步:实现混合检索

生成 query vector 后,同时执行 dense 与 BM25:

denseReq := milvusclient.NewAnnRequest(
	"dense_vector",
	candidates,
	entity.FloatVector(queryVector),
).WithFilter(filter)

sparseReq := milvusclient.NewAnnRequest(
	"sparse_vector",
	candidates,
	entity.Text(query),
).WithFilter(filter)

results, err := client.HybridSearch(
	ctx,
	milvusclient.NewHybridSearchOption(
		collection,
		candidates,
		denseReq,
		sparseReq,
	).
		WithOutputFields(
			"doc_id",
			"title",
			"source",
			"content",
		).
		WithReranker(
			milvusclient.NewRRFReranker(),
		),
)

Dense 检索处理语义相近表达,BM25 擅长产品名、政策术语和精确关键词。RRF 根据两路排名融合候选,应用层再根据 query 与标题、正文的词项重合进行轻量 rerank。

Milvus 集成测试应覆盖:

  • 相关问题返回正确来源;
  • tenant A 无法获取 tenant B 文档;
  • TopK 生效;
  • 空知识库返回空结果;
  • embedding 失败和 Milvus 超时;
  • 中文产品名与政策关键词;
  • dense 与 BM25 单路弱、融合后命中的样本。

仓库中的 rag.MemoryStore 仅用于快速单元测试,它接收 tenantID 参数但当前没有按 tenant 过滤。生产检索使用 MilvusStore,由查询 filter 强制租户条件。MemoryStore 不能充当多租户安全边界;若要在服务代码或多租户测试中复用,需要为内存数据增加 tenant 字段并实现相同过滤语义。

第六步:实现业务服务

订单查询:

func (r *Repository) Order(
	customerID string,
	orderID string,
) (Order, error) {
	order, ok := r.orders[orderID]
	if !ok || order.CustomerID != customerID {
		return Order{}, ErrOrderNotFound
	}
	return order, nil
}

物流查询先验证订单归属,再使用订单关联的 tracking number 查询 Shipment。模型不能直接指定 tracking number 和 customer ID。

工单采用两阶段协议:

draft_ticket
  → 生成分类、优先级、摘要和 draft_id
  → Agent 展示草稿
  → 用户确认或取消
  → confirm_ticket
  → 创建 ticket_id 或返回取消状态

生产版本为 draft 增加:

  • 过期时间;
  • customer/tenant 归属;
  • 状态机;
  • 幂等键;
  • 创建者与确认者;
  • 审计记录。

第七步:把能力包装成 Tool

Tool参数权限与副作用
search_knowledgequery、top_ktenant filter,无副作用
query_orderorder_idcustomer ownership
query_logisticsorder_id先校验订单归属
draft_ticketcategory、priority、summary、order_id创建临时草稿
confirm_ticketdraft_id、approved明确确认后创建工单

知识 Tool:

type Dependencies struct {
	TenantID   string
	CustomerID string
	Knowledge  rag.Store
	Business   *business.Repository
	Tickets    *business.TicketService
}

searchKnowledge, err := utils.InferTool(
	"search_knowledge",
	"检索企业知识库中的退换货、保修、物流、产品使用和工单 SLA 资料,返回引用来源。涉及政策和产品说明时优先调用。",
	func(ctx context.Context, args searchKnowledgeArgs) (string, error) {
		limit := args.TopK
		if limit <= 0 {
			limit = 4
		}
		if limit > 6 {
			limit = 6
		}

		hits, err := deps.Knowledge.Search(
			ctx, deps.TenantID, args.Query, limit,
		)
		if err != nil {
			return "", err
		}
		return encode(struct {
			Query string    `json:"query"`
			Hits  []rag.Hit `json:"hits"`
		}{Query: args.Query, Hits: hits})
	},
)

配套 CLI 每次为一个 customer 创建 Runner,因此 tenant 和 customer identity 通过 Dependencies 闭包绑定到这组 Tool;模型生成的参数无法覆盖这两个身份。Tool 适配层负责参数范围限制、调用依赖和结果编码,订单归属、工单状态机等规则留在 business package。改造成多用户 HTTP 服务时,需要从经过认证的请求或 session 构造请求级依赖,或在安全中间件中注入身份,同时禁止复用其他用户绑定的 Runner 与 Tool 实例。

第八步:创建 ChatModelAgent

serviceAgent, err := adk.NewChatModelAgent(
	ctx,
	&adk.ChatModelAgentConfig{
		Name:        "enterprise_customer_service",
		Description: "企业智能客服与工单助手。",
		Instruction: instruction,
		Model:       cm,
		MaxIterations: 12,
		ToolsConfig: adk.ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools:               tools,
				ExecuteSequentially: true,
			},
		},
	},
)
if err != nil {
	return nil, err
}

runner := adk.NewRunner(ctx, adk.RunnerConfig{
	Agent:           serviceAgent,
	EnableStreaming: true,
})

Instruction 明确:

  • 政策问题必须检索;
  • 订单与物流字段必须调用 Tool;
  • 缺少订单号时追问;
  • 知识回答的引用格式;
  • 空召回和冲突资料的降级;
  • 工单提交前的确认;
  • 禁止编造政策、订单、物流与 ticket ID。

第九步:消费事件并维护历史

messages := append(clone(history), schema.UserMessage(query))
responses, err := serviceagent.ConsumeEvents(
	runner.Run(ctx, messages),
	output,
	debug,
)
if err != nil {
	return history, err
}
history = append(messages, responses...)

当前事件消费者处理:

  • 非流式 Message
  • MessageStream 的逐块输出、关闭与 schema.ConcatMessages 合并;
  • assistant 消息中的 ToolCalls 和 tool result 输出;
  • debug 模式下的 AgentAction
  • AgentEvent.Err

流结束后合并完整 Message,再写入 history。当前单 Agent CLI 没有消费 RunPathCustomizedOutput;接入 AgentTool、DeepAgent 或自定义事件后需要增加对应分支。未来裁剪历史时要保持 ToolCall 与 Tool Result 成对,避免产生孤立 tool message。

第十步:运行项目

启动 Milvus:

cd code/11-customer-service-assistant
docker compose up -d
docker compose ps

导入知识:

go run ./cmd/indexer --recreate

独立验证检索:

go run ./cmd/search \
  -query 'Aurora Pro 耳机拆封使用后,七天内可以无理由退货吗?' \
  -top-k 3

配置 ChatModel:

export OPENAI_API_KEY='your-api-key'
export OPENAI_BASE_URL='https://api.deepseek.com'
export OPENAI_MODEL_NAME='deepseek-chat'

启动客服:

go run ./cmd/assistant -customer-id CUST-001

推荐测试对话:

  1. Aurora Pro 耳机拆封使用后还能七天无理由退货吗?
  2. 订单 ORD-20260701-1001 现在是什么状态?
  3. 这个订单的包裹到哪里了?
  4. 物流两天没更新,帮我转人工处理。
  5. 信息正确,确认创建。

这组对话依次验证 RAG、引用、订单归属、跨轮指代、物流查询、工单草稿和明确确认。

测试与验收

仓库当前已有的自动化测试

go test -race ./...
go vet ./...

当前测试代码实际覆盖:

  • Markdown 按二级标题切块、来源字段和 chunk ID 唯一性;
  • hash embedding 的确定性、维度和 L2 归一化;
  • MemoryStore 的相关片段与引用召回;
  • Milvus 主键中的 tenant scope 与稳定性;
  • 订单 customer ownership;
  • 工单必须由草稿所属客户确认、草稿不可重复消费;
  • 取消草稿不会产生 ticket ID。

docker compose config --quiet 只检查 Compose 配置语法,可以作为本地静态检查运行:

docker compose config --quiet

需要真实 Milvus 和模型的手工验收

先启动 Milvus、导入知识、运行独立检索,再启动客服执行前述五轮对话。逐项核对:

  • 检索返回正确 title、source、content 和 score;
  • 所有知识查询包含 tenant filter;
  • 其他客户的订单返回不可见;
  • 物流 Tool 先校验订单归属;
  • 政策回答包含真实来源;
  • 空召回时没有编造答案;
  • 工单先创建草稿;
  • 明确确认后才产生 ticket ID;
  • 取消工单草稿不会创建 ticket;
  • history 包含 ToolCall、Tool Result 和最终回答;

生产化前需要补充的自动化测试

  • 使用真实 Milvus 的入库、dense + BM25、RRF、TopK、空召回和 tenant 隔离集成测试;
  • 五个 Eino Tool 的 Schema、参数边界、身份绑定、错误编码和超时测试;
  • 使用可控 fake ChatModel 覆盖完整 Agent ToolCall → Tool Result → 最终回答轨迹;
  • MessageStream 合并、ToolCall/Tool Result 成对写入 history 的测试;
  • 引用必须来自本轮检索结果的校验;
  • 达到 MaxIterations 时返回稳定失败信息;
  • 模型、Milvus 和 Tool 故障、超时、取消与降级路径;
  • MemoryStore 增加 tenant 过滤后再用于多租户语义测试。

将项目升级为完整 Eino 编排

当前项目用普通 Go 代码实现 RAG 内部步骤,便于学习 Milvus。下一步可以改造成 Workflow:

query
  ├─ rewrite
  ├─ dense embedding → dense search
  └─ original query → BM25 search
        RRF merge
          rerank
      citation formatter

编译后的 Workflow 再包装成 Graph Tool,ChatModelAgent 只看见一个 search_knowledge。这样能够同时获得:

  • Agent 的动态能力选择;
  • RAG 内部流程的确定性;
  • 节点级 Callback;
  • 独立测试与回放;
  • 清晰的超时与错误边界。

生产化升级

数据与权限:

  • fixtures 替换为真实订单、物流和工单服务;
  • 统一从鉴权层注入 user、tenant、role 和 request ID;
  • 文档加入 ACL、状态、版本和生效时间;
  • Tool 参数、结果和日志执行脱敏。

RAG 质量:

  • 使用企业 embedding 和 cross-encoder reranker;
  • 建立问题、期望来源和期望回答数据集;
  • 测量 dense recall、BM25 recall、融合 recall 与 NDCG;
  • 为低置信度、冲突和过期知识定义降级。

会话与副作用:

  • history 迁移到持久化 Session;
  • 工单草稿增加过期与幂等;
  • 长时间确认使用 Interrupt/Resume;
  • 持续输入使用 TurnLoop。

可观测与可靠性:

  • model、retriever、tool 建立子 span;
  • 记录 token、首帧延迟、迭代次数和错误率;
  • 模型与 Tool 分别设置超时、重试和熔断;
  • 发布前运行离线评估;
  • 线上抽样回放失败 Trace。

本章小结

主题结论
四层架构组件、编排、Agent、DevOps 按单向依赖逐层组合
组件层标准化模型、Prompt、Tool、文档与检索
Agentic 组件用有序 ContentBlock 表达 provider 原生 Agent 数据
编排层Chain 处理线性流程,Graph 处理分支、并行和循环
Flow把 ReAct、Host Multi-Agent、多查询和父子文档等通用模式封装为可复用实现
ADK封装 ReAct、Runner、事件、状态、工具调度和多 Agent
EinoExt提供具体组件、Callback 与 DevOps 实现
智能客服外层 Agent 选择能力,内层确定性流程保证业务可控

思考题

  1. ChatModel、ChatTemplate 和 AgenticModel 的类型边界分别是什么?
  2. tenant filter 应在哪些服务端位置强制执行?
  3. Graph 中哪个节点会结束全链路的低首帧延迟?
  4. query_order 与 confirm_ticket 应使用怎样的不同重试策略?
  5. AgentAsTool 如何减少多 Agent 上下文污染?
  6. Prompt、模型、知识库和 Tool 版本如何共同进入评估记录?
  7. 工单审批跨越进程重启时,Session、Checkpoint 与幂等键如何关联?
  8. MultiQuery Retriever、Router Retriever 和 Parent Retriever 分别改善 RAG 的哪个环节?

参考资料

最后更新于 • Q1mi