vault backup: 2026-04-29 18:36:58
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-16"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 快速开始
|
||||
weight: 2
|
||||
---
|
||||
|
||||
本篇文档用于作为 ChatWithEino Quickstart 的统一入口:用一条清晰的路径带你跑起来,并解释这个系列最终要交付什么(一个可扩展的端到端 Agent 应用骨架)。
|
||||
|
||||
## 这是什么
|
||||
|
||||
ChatWithEino 是一个基于 Eino 构建的学习型 Agent:它能读取源码/文档/示例,并通过对话帮助开发者理解 Eino 以及用 Eino 写代码。
|
||||
|
||||
这个 Quickstart 系列采用“渐进式搭建”的方式:
|
||||
|
||||
- 前期以 Console 为载体,逐步引入 ChatModel、Agent/Runner、Memory、Tool、Middleware、Callback、Interrupt/Resume、Graph Tool、Skill
|
||||
- 最终把同一个 Agent 以 Web 形态交付出来,并用 A2UI 协议把事件流渲染成可增量更新的 UI
|
||||
|
||||
## 最短路径:先跑起来
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/cloudwego/eino-examples.git
|
||||
cd eino-examples/quickstart/chatwitheino
|
||||
```
|
||||
|
||||
### 1) 最小 Console(第一章)
|
||||
|
||||
准备模型配置(以 OpenAI 为例):
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="..."
|
||||
export OPENAI_MODEL="gpt-4.1-mini"
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
go run ./cmd/ch01 -- "用一句话解释 Eino 的 Component 设计解决了什么问题?"
|
||||
```
|
||||
|
||||
### 2) 最终 Web(A2UI)
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
启动后访问输出里的地址(默认 `http://localhost:8080`)。
|
||||
|
||||
### 3) (可选)开启 skills(第九章能力复用)
|
||||
|
||||
skills 用于把一组稳定的“知识/指令包”(`SKILL.md` + `reference/*.md`)注入到 Agent,让模型在需要时按需加载并调用。
|
||||
|
||||
```bash
|
||||
go run ./scripts/sync_eino_ext_skills.go -src /path/to/eino-ext -dest ./skills/eino-ext -clean
|
||||
EINO_EXT_SKILLS_DIR="$(pwd)/skills/eino-ext" go run .
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `./skills/` 目录默认被 `.gitignore` 忽略,避免把同步出来的 skills 误提交
|
||||
- 如需验证 Skill 是否生效,可运行第九章示例入口代码:
|
||||
- [https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch09/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch09/main.go)
|
||||
|
||||
## 学习路线(章节导航)
|
||||
|
||||
<table>
|
||||
<tr><td>章节</td><td>主题</td><td>入口</td></tr>
|
||||
<tr><td>第一章</td><td>ChatModel 与 Message(Console)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch01_chatmodel_agent_console.md</td></tr>
|
||||
<tr><td>第二章</td><td>Agent 与 Runner(Console 多轮)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch02_chatmodel_agent_runner_console.md</td></tr>
|
||||
<tr><td>第三章</td><td>Memory 与 Session(持久化对话)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch03_memory_session_jsonl.md</td></tr>
|
||||
<tr><td>第四章</td><td>Tool 与文件系统访问</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch04_tool_backend_filesystem.md</td></tr>
|
||||
<tr><td>第五章</td><td>Middleware(中间件模式)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch05_middleware.md</td></tr>
|
||||
<tr><td>第六章</td><td>Callback 与 Trace(可观测性)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch06_callback.md</td></tr>
|
||||
<tr><td>第七章</td><td>Interrupt/Resume(中断与恢复)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch07_interrupt_resume.md</td></tr>
|
||||
<tr><td>第八章</td><td>Graph Tool(复杂工作流)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch08_graph_tool.md</td></tr>
|
||||
<tr><td>第九章</td><td>Skill(Console)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch09_skill.md</td></tr>
|
||||
<tr><td>最终章</td><td>A2UI(Web)</td><td>https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/docs/ch10_a2ui.md</td></tr>
|
||||
</table>
|
||||
|
||||
## 最终交付:一个可扩展的端到端 Agent 应用骨架
|
||||
|
||||
你可以把这个 Quickstart 的最终产物理解为一套“可插拔的应用骨架”,它把 Eino 的关键能力连成闭环:
|
||||
|
||||
- 运行时:Runner 驱动执行,支持流式输出与事件模型
|
||||
- 工具层:通过 Tool 接入文件系统/检索/工作流等能力
|
||||
- 中间件:用 handler/middleware 承载重试、审批、错误处理等横切能力
|
||||
- 人机协作:interrupt/resume + checkpoint 支持审批、补参、分支选择等交互式流程
|
||||
- 确定性编排:compose(graph/chain/workflow)把复杂业务流程组织为可维护、可复用的执行图
|
||||
- UI 交付:用 A2UI 把 Agent 的事件流映射为可增量渲染的 UI 组件树(SSE 推送)
|
||||
|
||||
其中 A2UI 的边界需要明确:它不是 Eino 框架本身的一部分,而是业务层的 UI 协议/渲染方案。本 Quickstart 用它来展示“Agent 能力如何以产品形态呈现给用户”,具体实现与协议细节以最终章为准。
|
||||
|
||||
## 下一步探索(从 Quickstart 到真实业务)
|
||||
|
||||
- 想系统理解 Eino 的组件抽象与用法:从第一章的 Component 入门开始,再按章节逐步补齐 Tool/Graph/Callback/Interrupt 等能力
|
||||
- 想复用更大规模的知识与指令:对接 `eino-ext` 的 skills,并通过 Skill 中间件按需加载
|
||||
- 想把 Agent 做成业务产品:参考最终章(A2UI/Web)把事件流、状态与交互打通,再替换为你自己的 UI 形态与协议
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-24"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第一章:ChatModel 与 Message(Console)
|
||||
weight: 1
|
||||
---
|
||||
|
||||
## Eino 框架简介
|
||||
|
||||
**Eino 是什么?**
|
||||
|
||||
Eino 是一个 Go 语言实现的 AI 应用开发框架(Agent Development Kit),旨在帮助开发者快速构建可扩展、可维护的 AI 应用。
|
||||
|
||||
**Eino 解决什么问题?**
|
||||
|
||||
1. **模型抽象**:统一不同 LLM 提供商的接口(OpenAI、Ark、Claude 等),切换模型无需修改业务代码
|
||||
2. **能力组合**:通过 Component 接口实现可替换、可组合的能力单元(对话、工具、检索等)
|
||||
3. **编排框架**:提供 Agent、Graph、Chain 等编排抽象,支持复杂的多步骤 AI 工作流
|
||||
4. **运行时支持**:内置流式输出、中断与恢复、状态管理、Callback 可观测性等能力
|
||||
|
||||
**Eino 的主要仓库:**
|
||||
|
||||
- **eino**(本仓库):核心库,定义接口、编排抽象和 ADK
|
||||
- **eino-ext**:扩展库,提供各类 Component 的具体实现(OpenAI、Ark、Milvus 等)
|
||||
- **eino-examples**:示例代码库,包含本 quickstart 系列
|
||||
|
||||
---
|
||||
|
||||
## ChatWithEino:与 Eino 文档对话的智能助手
|
||||
|
||||
**ChatWithEino 是什么?**
|
||||
|
||||
ChatWithEino 是一个基于 Eino 框架构建的智能助手,能够帮助开发者学习 Eino 框架并编写 Eino 代码。它通过访问 Eino 仓库的源码、注释和示例,为用户提供最准确、最及时的技术支持。
|
||||
|
||||
**核心能力:**
|
||||
|
||||
- **对话交互**:理解用户关于 Eino 的问题,提供清晰的解答
|
||||
- **代码访问**:直接读取 Eino 源码、注释和示例,基于真实实现回答问题
|
||||
- **持久化会话**:支持多轮对话,记住上下文,可跨进程恢复会话
|
||||
- **工具调用**:能够执行文件读取、代码搜索等操作
|
||||
|
||||
**技术架构:**
|
||||
|
||||
- **ChatModel**:与大语言模型通信(OpenAI、Ark、Claude 等)
|
||||
- **Tool**:文件系统访问、代码搜索等能力扩展
|
||||
- **Memory**:对话历史持久化存储
|
||||
- **Agent**:统一的执行框架,协调各组件协同工作
|
||||
|
||||
## Quickstart 文档系列:从零构建 ChatWithEino
|
||||
|
||||
本系列文档通过循序渐进的方式,带你从最基础的 ChatModel 调用开始,逐步构建一个功能完整的 ChatWithEino Agent。
|
||||
|
||||
**学习路径:**
|
||||
|
||||
<table>
|
||||
<tr><td>章节</td><td>主题</td><td>核心内容</td><td>能力提升</td></tr>
|
||||
<tr><td><strong>第一章</strong></td><td>ChatModel 与 Message</td><td>理解 Component 抽象,实现单次对话</td><td>基础对话能力</td></tr>
|
||||
<tr><td><strong>第二章</strong></td><td>Agent 与 Runner</td><td>引入执行抽象,实现多轮对话</td><td>会话管理能力</td></tr>
|
||||
<tr><td><strong>第三章</strong></td><td>Memory 与 Session</td><td>持久化对话历史,支持会话恢复</td><td>持久化能力</td></tr>
|
||||
<tr><td><strong>第四章</strong></td><td>Tool 与文件系统</td><td>添加文件访问能力,读取源码</td><td>工具调用能力</td></tr>
|
||||
<tr><td><strong>第五章</strong></td><td>Middleware</td><td>中间件机制,统一处理横切关注点</td><td>扩展性增强</td></tr>
|
||||
<tr><td><strong>第六章</strong></td><td>Callback</td><td>回调机制,监控 Agent 执行过程</td><td>可观测性</td></tr>
|
||||
<tr><td><strong>第七章</strong></td><td>Interrupt 与 Resume</td><td>中断与恢复,支持长时间任务</td><td>可靠性增强</td></tr>
|
||||
<tr><td><strong>第八章</strong></td><td>Graph 与 Tool</td><td>使用 Graph 编排复杂工作流</td><td>复杂编排能力</td></tr>
|
||||
<tr><td><strong>第九章</strong></td><td>A2UI</td><td>Agent 到 UI 的集成方案</td><td>生产级应用</td></tr>
|
||||
</table>
|
||||
|
||||
**为什么这样设计?**
|
||||
|
||||
每一章都在前一章的基础上增加一个核心能力,让你:
|
||||
|
||||
1. **理解每个组件的作用**:不是一次性展示所有功能,而是逐步引入
|
||||
2. **看到架构演进过程**:从简单到复杂,理解为什么需要每个抽象
|
||||
3. **掌握实际开发技能**:每章都有可运行的代码,可以动手实践
|
||||
|
||||
---
|
||||
|
||||
本章目标:理解 Eino 的 Component 抽象,用最小代码调用一次 ChatModel(支持流式输出),并掌握 `schema.Message` 的基本用法。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch01/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch01/main.go)
|
||||
|
||||
## 为什么需要 Component 接口
|
||||
|
||||
Eino 定义了一组 Component 接口(`ChatModel`、`Tool`、`Retriever`、`Loader` 等),每个接口描述一类可替换的能力:
|
||||
|
||||
```go
|
||||
type BaseChatModel interface {
|
||||
Generate(ctx context.Context, input []*schema.Message, opts ...Option) (*schema.Message, error)
|
||||
Stream(ctx context.Context, input []*schema.Message, opts ...Option) (
|
||||
*schema.StreamReader[*schema.Message], error)
|
||||
}
|
||||
```
|
||||
|
||||
**接口带来的好处:**
|
||||
|
||||
1. **实现可替换**:`eino-ext` 提供了 OpenAI、Ark、Claude、Ollama 等多种实现,业务代码只依赖接口,切换模型只需改构造逻辑。
|
||||
2. **编排可组合**:Agent、Graph、Chain 等编排层只依赖 Component 接口,不关心具体实现。你可以把 OpenAI 换成 Ark,编排代码无需改动。
|
||||
3. **测试可 Mock**:接口天然支持 mock,单元测试不需要真实调用模型。
|
||||
|
||||
本章只涉及 `ChatModel`,后续章节会逐步引入 `Tool`、`Retriever` 等 Component。
|
||||
|
||||
## schema.Message:对话的基本单位
|
||||
|
||||
`Message` 是 Eino 里对话数据的基本结构:
|
||||
|
||||
```go
|
||||
type Message struct {
|
||||
Role RoleType // system / user / assistant / tool
|
||||
Content string // 文本内容
|
||||
ToolCalls []ToolCall // 仅 assistant 消息可能有
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
常用构造函数:
|
||||
|
||||
```go
|
||||
schema.SystemMessage("You are a helpful assistant.")
|
||||
schema.UserMessage("What is the weather today?")
|
||||
schema.AssistantMessage("I don't know.", nil) // 第二个参数是 ToolCalls
|
||||
schema.ToolMessage("tool result", "call_id")
|
||||
```
|
||||
|
||||
**角色语义:**
|
||||
|
||||
- `system`:系统指令,通常放在 messages 最前面
|
||||
- `user`:用户输入
|
||||
- `assistant`:模型回复
|
||||
- `tool`:工具调用结果(后续章节涉及)
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 获取代码
|
||||
|
||||
```bash
|
||||
git clone https://github.com/cloudwego/eino-examples.git
|
||||
cd eino-examples/quickstart/chatwitheino
|
||||
```
|
||||
|
||||
- Go 版本:Go 1.21+(见 `go.mod`)
|
||||
- 一个可调用的 ChatModel(默认使用 OpenAI;也支持 Ark)
|
||||
|
||||
### 方式 A:OpenAI(默认)
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="..."
|
||||
export OPENAI_MODEL="gpt-4.1-mini" # OpenAI 2025 年新模型,也可用 gpt-4o、gpt-4o-mini 等
|
||||
# 可选:
|
||||
# OPENAI_BASE_URL(代理或兼容服务)
|
||||
# OPENAI_BY_AZURE=true(使用 Azure OpenAI)
|
||||
```
|
||||
|
||||
### 方式 B:Ark
|
||||
|
||||
```bash
|
||||
export MODEL_TYPE="ark"
|
||||
export ARK_API_KEY="..."
|
||||
export ARK_MODEL="..."
|
||||
# 可选:ARK_BASE_URL
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
go run ./cmd/ch01 -- "用一句话解释 Eino 的 Component 设计解决了什么问题?"
|
||||
```
|
||||
|
||||
输出示例(流式逐步打印):
|
||||
|
||||
```
|
||||
[assistant] Eino 的 Component 设计通过定义统一接口...
|
||||
```
|
||||
|
||||
## 入口代码做了什么
|
||||
|
||||
按执行顺序:
|
||||
|
||||
1. **创建 ChatModel**:根据 `MODEL_TYPE` 环境变量选择 OpenAI 或 Ark 实现
|
||||
2. **构造输入 messages**:`SystemMessage(instruction)` + `UserMessage(query)`
|
||||
3. **调用 Stream**:所有 ChatModel 实现都必须支持 `Stream()`,返回 `StreamReader[*Message]`
|
||||
4. **打印结果**:迭代 `StreamReader` 逐帧打印 assistant 回复
|
||||
|
||||
关键代码片段(**注意:这是简化后的代码片段,不能直接运行****,完整代码请参考** [cmd/ch01/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch01/main.go)):
|
||||
|
||||
```go
|
||||
// 构造输入
|
||||
messages := []*schema.Message{
|
||||
schema.SystemMessage(instruction),
|
||||
schema.UserMessage(query),
|
||||
}
|
||||
|
||||
// 调用 Stream(所有 ChatModel 都必须实现)
|
||||
stream, err := cm.Stream(ctx, messages)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Print(chunk.Content)
|
||||
}
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Component 接口**:定义可替换、可组合、可测试的能力边界
|
||||
- **Message**:对话数据的基本单位,通过角色区分语义
|
||||
- **ChatModel**:最基础的 Component,提供 `Generate` 和 `Stream` 两个核心方法
|
||||
- **实现选择**:通过环境变量或配置切换 OpenAI/Ark 等不同实现,业务代码无需改动
|
||||
@@ -0,0 +1,309 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-12"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第二章:ChatModelAgent、Runner、AgentEvent(Console 多轮)
|
||||
weight: 2
|
||||
---
|
||||
|
||||
本章目标:引入 ADK 的执行抽象(Agent + Runner),并用一个 Console 程序实现多轮对话。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch02/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch02/main.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
go run ./cmd/ch02
|
||||
```
|
||||
|
||||
看到提示后输入问题(空行退出):
|
||||
|
||||
```
|
||||
you> 你好,解释一下 Eino 里的 Agent 是什么?
|
||||
...
|
||||
you> 再用一句话总结一下
|
||||
...
|
||||
```
|
||||
|
||||
## 关键概念
|
||||
|
||||
### 从 Component 到 Agent
|
||||
|
||||
第一章我们学习了 **Component**(组件),它是 Eino 中可替换、可组合的能力单元:
|
||||
|
||||
- `ChatModel`:调用大语言模型
|
||||
- `Tool`:执行特定任务
|
||||
- `Retriever`:检索信息
|
||||
- `Loader`:加载数据
|
||||
|
||||
**Component 和 Agent 的关系:**
|
||||
|
||||
- **Component 不构成完整的 AI 应用**:它只是能力单元,需要被组织、编排、执行
|
||||
- **Agent 是完整的 AI 应用**:它封装了完整的业务逻辑,可以直接运行
|
||||
- **Agent 内部使用 Component**:最核心的是 `ChatModel`(对话能力)和 `Tool`(执行能力)
|
||||
|
||||
**为什么需要 Agent?**
|
||||
|
||||
如果只有 Component,你需要自己:
|
||||
|
||||
- 管理对话历史
|
||||
- 编排调用流程(何时调用模型、何时调用工具)
|
||||
- 处理流式输出
|
||||
- 实现中断恢复
|
||||
- ...
|
||||
|
||||
**Agent 提供了什么?**
|
||||
|
||||
- **完整的运行时框架**:通过 `Runner` 统一管理执行过程
|
||||
- **标准的事件流输出**:`Run() -> AsyncIterator[*AgentEvent]`,支持流式、中断、恢复
|
||||
- **可扩展能力**:可以添加 tools、middleware、interrupt 等
|
||||
- **开箱即用**:创建 Agent 后直接运行,无需关心内部细节
|
||||
|
||||
**本章示例:**
|
||||
|
||||
`ChatModelAgent` 是最简单的 Agent,它内部只使用了 `ChatModel`,但已经具备了 Agent 的完整能力框架。后续章节会展示如何添加 `Tool` 等更多能力。
|
||||
|
||||
### Agent 接口
|
||||
|
||||
`Agent` 是 ADK 中的核心接口,定义了智能体的基本行为:
|
||||
|
||||
```go
|
||||
type Agent interface {
|
||||
Name(ctx context.Context) string
|
||||
Description(ctx context.Context) string
|
||||
|
||||
// Run 执行 Agent,返回事件流
|
||||
Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
|
||||
}
|
||||
```
|
||||
|
||||
**接口职责:**
|
||||
|
||||
- `Name()` / `Description()`:标识 Agent 的名称和描述
|
||||
- `Run()`:执行 Agent 的核心方法,接收输入消息,返回事件流
|
||||
|
||||
**设计理念:**
|
||||
|
||||
- **统一抽象**:所有 Agent(ChatModelAgent、WorkflowAgent、SupervisorAgent 等)都实现这个接口
|
||||
- **事件驱动**:通过事件流(`AsyncIterator[*AgentEvent]`)输出执行过程,支持流式响应
|
||||
- **可扩展性**:后续加入 tools、middleware、interrupt 等能力时,接口保持不变
|
||||
|
||||
### ChatModelAgent
|
||||
|
||||
`ChatModelAgent` 是 Agent 接口的一个实现,基于 ChatModel 构建:
|
||||
|
||||
```go
|
||||
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||
Name: "Ch02ChatModelAgent",
|
||||
Description: "A minimal ChatModelAgent with in-memory multi-turn history.",
|
||||
Instruction: instruction,
|
||||
Model: cm,
|
||||
})
|
||||
```
|
||||
|
||||
**ChatModel vs ChatModelAgent:本质区别**
|
||||
|
||||
<table>
|
||||
<tr><td>维度</td><td>ChatModel</td><td>ChatModelAgent</td></tr>
|
||||
<tr><td><strong>定位</strong></td><td>Component(组件)</td><td>Agent(智能体)</td></tr>
|
||||
<tr><td><strong>接口</strong></td><td><pre>Generate() / Stream()</pre></td><td><pre>Run() -> AsyncIterator[*AgentEvent]</pre></td></tr>
|
||||
<tr><td><strong>输出</strong></td><td>直接返回消息内容</td><td>返回事件流(包含消息、控制动作等)</td></tr>
|
||||
<tr><td><strong>能力</strong></td><td>单纯的模型调用</td><td>可扩展 tools、middleware、interrupt 等</td></tr>
|
||||
<tr><td><strong>适用场景</strong></td><td>简单的对话场景</td><td>复杂的智能体应用</td></tr>
|
||||
</table>
|
||||
|
||||
**为什么需要 ChatModelAgent?**
|
||||
|
||||
1. **统一抽象**:ChatModel 只是 Component 的一种,而 Agent 是更高层的抽象,可以组合多种 Component
|
||||
2. **事件驱动**:Agent 输出事件流,支持流式响应、中断恢复、状态转移等复杂场景
|
||||
3. **可扩展性**:ChatModelAgent 可以添加 tools、middleware、interrupt 等能力,而 ChatModel 只能调用模型
|
||||
4. **编排友好**:Agent 可以被 Runner 统一管理,支持 checkpoint、恢复等运行时能力
|
||||
|
||||
**简单来说:**
|
||||
|
||||
- **ChatModel** = "负责与大语言模型通信的组件,屏蔽不同模型提供商的差异(OpenAI、Ark、Claude 等)"
|
||||
- **ChatModelAgent** = "基于模型构建的智能体,可以调用模型,但还能做更多事"
|
||||
|
||||
**类比理解:**
|
||||
|
||||
- **ChatModel** 就像"数据库驱动":负责与数据库通信,屏蔽 MySQL/PostgreSQL 的差异
|
||||
- **ChatModelAgent** 就像"业务逻辑层":基于数据库驱动构建,但还包含业务规则、事务管理等
|
||||
|
||||
**特点:**
|
||||
|
||||
- 封装了 ChatModel 的调用逻辑
|
||||
- 提供统一的 `Run() -> AgentEvent` 输出形态
|
||||
- 后续可以添加 tools、middleware 等能力
|
||||
|
||||
### Runner
|
||||
|
||||
`Runner` 是执行 Agent 的入口点,负责管理 Agent 的生命周期:
|
||||
|
||||
```go
|
||||
type Runner struct {
|
||||
a Agent // 要执行的 Agent
|
||||
enableStreaming bool
|
||||
store CheckPointStore // 用于中断恢复的状态存储
|
||||
}
|
||||
```
|
||||
|
||||
**为什么需要 Runner?**
|
||||
|
||||
虽然 Agent 提供了 `Run()` 方法,但直接调用会缺少很多运行时能力:
|
||||
|
||||
1. **生命周期管理**:Runner 管理 Agent 的启动、恢复、中断等状态
|
||||
2. **Checkpoint 支持**:配合 `CheckPointStore` 实现中断恢复(后续章节涉及)
|
||||
3. **统一入口**:提供 `Run()` 和 `Query()` 等便捷方法
|
||||
4. **事件流封装**:将 Agent 的事件流转换为可消费的 `AsyncIterator[*AgentEvent]`
|
||||
|
||||
**使用方式:**
|
||||
|
||||
```go
|
||||
runner := adk.NewRunner(ctx, adk.RunnerConfig{
|
||||
Agent: agent,
|
||||
EnableStreaming: true,
|
||||
})
|
||||
|
||||
// 方式 1:传入消息列表
|
||||
events := runner.Run(ctx, history)
|
||||
|
||||
// 方式 2:便捷方法,传入单个查询字符串
|
||||
events := runner.Query(ctx, "你好")
|
||||
```
|
||||
|
||||
### AgentEvent
|
||||
|
||||
`AgentEvent` 是 Runner 返回的事件单元:
|
||||
|
||||
```go
|
||||
type AgentEvent struct {
|
||||
AgentName string
|
||||
RunPath []RunStep
|
||||
|
||||
Output *AgentOutput // 输出内容
|
||||
Action *AgentAction // 控制动作
|
||||
Err error // 执行错误
|
||||
}
|
||||
```
|
||||
|
||||
**主要字段:**
|
||||
|
||||
- `event.Err`:执行错误
|
||||
- `event.Output.MessageOutput`:message 或 message stream(流式)
|
||||
- `event.Action`:中断/转移/退出等控制动作(后续章节用到)
|
||||
|
||||
### AsyncIterator:事件流的消费方式
|
||||
|
||||
`Runner.Run()` 返回的是 `*AsyncIterator[*AgentEvent]`,这是一个非阻塞的流式迭代器。
|
||||
|
||||
**为什么用 AsyncIterator 而不是直接返回结果?**
|
||||
|
||||
因为 Agent 的执行是**流式**的:模型逐 token 生成回复,Tool 调用穿插其中。如果等全部完成再返回,用户需要等待更长时间。`AsyncIterator` 让你可以实时消费每一个事件。
|
||||
|
||||
**消费方式:**
|
||||
|
||||
```go
|
||||
// events 是 *AsyncIterator[*AgentEvent],由 runner.Run() 返回
|
||||
events := runner.Run(ctx, history)
|
||||
|
||||
for {
|
||||
event, ok := events.Next() // 获取下一个事件,阻塞直到有事件或结束
|
||||
if !ok {
|
||||
break // 迭代器关闭,全部事件已消费
|
||||
}
|
||||
if event.Err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
if event.Output != nil && event.Output.MessageOutput != nil {
|
||||
// 处理消息输出(可能是流式)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意:**每次 `runner.Run()` 创建新的迭代器,消费一次后不可重复使用。
|
||||
|
||||
## 多轮对话的实现
|
||||
|
||||
本章实现的是简单的多轮对话:用户输入 → 模型回复 → 用户继续输入 → ...
|
||||
|
||||
**实现方式:**
|
||||
|
||||
没有 tools 时,`ChatModelAgent` 在一次 `Run()` 里只会完成一轮模型调用。多轮对话是通过调用侧维护 history 实现的:
|
||||
|
||||
1. 用 `history []*schema.Message` 保存累计对话
|
||||
2. 每次用户输入:把 `UserMessage` 追加到 history
|
||||
3. 调用 `runner.Run(ctx, history)` 得到事件流,消费得到 assistant 文本
|
||||
4. 把本轮 assistant 文本追加回 history,进入下一轮
|
||||
|
||||
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch02/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch02/main.go)):
|
||||
|
||||
```go
|
||||
history := make([]*schema.Message, 0, 16)
|
||||
|
||||
for {
|
||||
// 1. 读取用户输入
|
||||
line := readUserInput()
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// 2. 追加用户消息到 history
|
||||
history = append(history, schema.UserMessage(line))
|
||||
|
||||
// 3. 调用 Runner 执行 Agent
|
||||
events := runner.Run(ctx, history)
|
||||
|
||||
// 4. 消费事件流,收集 assistant 回复
|
||||
content := collectAssistantFromEvents(events)
|
||||
|
||||
// 5. 追加 assistant 消息到 history
|
||||
history = append(history, schema.AssistantMessage(content, nil))
|
||||
}
|
||||
```
|
||||
|
||||
**流程图:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 初始化 history = [] │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 用户输入 UserMessage │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 追加到 history │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ runner.Run(history) │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 消费事件流 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 追加 AssistantMessage│
|
||||
└──────────────────────┘
|
||||
↓
|
||||
(循环继续)
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Agent 接口**:定义智能体的基本行为,核心是 `Run() -> AsyncIterator[*AgentEvent]`
|
||||
- **ChatModelAgent**:基于 ChatModel 实现的 Agent,提供统一的执行抽象
|
||||
- **Runner**:Agent 的执行入口,管理生命周期、checkpoint、事件流等运行时能力
|
||||
- **AgentEvent**:事件驱动的输出单元,支持流式响应和控制动作
|
||||
- **多轮对话**:通过调用侧维护 history 实现,每次 `Run()` 完成一轮对话
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-12"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第三章:Memory 与 Session(持久化对话)
|
||||
weight: 3
|
||||
---
|
||||
|
||||
本章目标:实现对话历史的持久化存储,支持跨进程恢复会话。
|
||||
|
||||
> **⚠️ 重要说明:业务层概念 vs 框架概念**
|
||||
|
||||
> 本章介绍的 **Memory、Session、Store 是业务层概念**,**不是 Eino 框架的核心组件**。
|
||||
|
||||
>
|
||||
|
||||
> 换句话说,Eino 框架只负责"如何处理消息",而"如何存储消息"完全由业务层决定。本章提供的实现只是一个简单的参考示例,你可以根据自己的业务需求选择完全不同的存储方案(数据库、Redis、云存储等)。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch03/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch03/main.go)
|
||||
- Memory 实现:[mem/store.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/mem/store.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
# 创建新会话
|
||||
go run ./cmd/ch03
|
||||
|
||||
# 恢复已有会话
|
||||
go run ./cmd/ch03 --session <session-id>
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
Created new session: 083d16da-6b13-4fe6-afb0-c45d8f490ce1
|
||||
Session title: New Session
|
||||
Enter your message (empty line to exit):
|
||||
you> 你好,我是张三
|
||||
[assistant] 你好张三!很高兴认识你...
|
||||
you> 我叫什么名字?
|
||||
[assistant] 你叫张三...
|
||||
|
||||
Session saved: 083d16da-6b13-4fe6-afb0-c45d8f490ce1
|
||||
Resume with: go run ./cmd/ch03 --session 083d16da-6b13-4fe6-afb0-c45d8f490ce1
|
||||
```
|
||||
|
||||
## 从内存到持久化:为什么需要 Memory
|
||||
|
||||
第二章我们实现了多轮对话,但有一个问题:**对话历史只存在于内存中**。
|
||||
|
||||
**内存存储的局限:**
|
||||
|
||||
- 进程退出后,对话历史丢失
|
||||
- 无法跨设备、跨进程恢复会话
|
||||
- 无法实现会话管理(列表、删除、搜索等)
|
||||
|
||||
**Memory 的定位:**
|
||||
|
||||
- **Memory 是对话历史的持久化存储**:将对话保存到磁盘或数据库
|
||||
- **Memory 支持 Session 管理**:每个 Session 代表一次完整的对话
|
||||
- **Memory 与 Agent 解耦**:Agent 不关心存储细节,只关心消息列表
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **内存存储** = "草稿纸"(进程退出就没了)
|
||||
- **Memory** = "笔记本"(永久保存,随时翻阅)
|
||||
|
||||
## 关键概念
|
||||
|
||||
> **再次强调**:以下 Session、Store 等概念都是**业务层实现**,用于管理对话历史的存储。Eino 框架本身不提供这些组件,而是由业务层负责管理消息列表,然后将消息传递给 `adk.Runner` 进行处理。
|
||||
|
||||
### Session(业务层概念)
|
||||
|
||||
`Session` 代表一次完整的对话会话:
|
||||
|
||||
```go
|
||||
type Session struct {
|
||||
ID string
|
||||
CreatedAt time.Time
|
||||
|
||||
messages []*schema.Message // 对话历史
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法:**
|
||||
|
||||
- `Append(msg)`:追加消息到会话,并持久化
|
||||
- `GetMessages()`:获取所有消息
|
||||
- `Title()`:从第一条用户消息生成会话标题
|
||||
|
||||
### Store(业务层概念)
|
||||
|
||||
`Store` 管理多个 Session 的持久化存储:
|
||||
|
||||
```go
|
||||
type Store struct {
|
||||
dir string // 存储目录
|
||||
cache map[string]*Session // 内存缓存
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法:**
|
||||
|
||||
- `GetOrCreate(id)`:获取或创建 Session
|
||||
- `List()`:列出所有 Session
|
||||
- `Delete(id)`:删除 Session
|
||||
|
||||
### JSONL 文件格式
|
||||
|
||||
每个 Session 存储为一个 `.jsonl` 文件:
|
||||
|
||||
```
|
||||
{"type":"session","id":"083d16da-...","created_at":"2026-03-11T10:00:00Z"}
|
||||
{"role":"user","content":"你好,我是谁?"}
|
||||
{"role":"assistant","content":"你好!我暂时不知道你是谁..."}
|
||||
{"role":"user","content":"我叫张三"}
|
||||
{"role":"assistant","content":"好的,张三,很高兴认识你!"}
|
||||
```
|
||||
|
||||
**为什么用 JSONL?**
|
||||
|
||||
- **简单**:每行一个 JSON 对象,易于读写
|
||||
- **可扩展**:可以追加新消息,无需重写整个文件
|
||||
- **可读性好**:可以用文本编辑器直接查看
|
||||
- **容错性强**:单行损坏不影响其他行
|
||||
|
||||
## Memory 的实现(业务层示例)
|
||||
|
||||
以下是一个简单的业务层实现示例,使用 JSONL 文件存储对话历史。这只是众多可能实现中的一种,你可以根据实际需求选择数据库、Redis 等其他存储方案。
|
||||
|
||||
### 1. 创建 Store
|
||||
|
||||
```go
|
||||
sessionDir := "./data/sessions"
|
||||
store, err := mem.NewStore(sessionDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取或创建 Session
|
||||
|
||||
```go
|
||||
sessionID := "083d16da-6b13-4fe6-afb0-c45d8f490ce1"
|
||||
session, err := store.GetOrCreate(sessionID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 追加用户消息
|
||||
|
||||
```go
|
||||
userMsg := schema.UserMessage("你好")
|
||||
if err := session.Append(userMsg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 获取历史并调用 Agent
|
||||
|
||||
```go
|
||||
history := session.GetMessages()
|
||||
events := runner.Run(ctx, history)
|
||||
content := collectAssistantFromEvents(events)
|
||||
```
|
||||
|
||||
### 5. 追加助手消息
|
||||
|
||||
```go
|
||||
assistantMsg := schema.AssistantMessage(content, nil)
|
||||
if err := session.Append(assistantMsg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch03/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch03/main.go)):
|
||||
|
||||
```go
|
||||
// 创建或恢复 Session
|
||||
session, err := store.GetOrCreate(sessionID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 用户输入
|
||||
userMsg := schema.UserMessage(line)
|
||||
if err := session.Append(userMsg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 调用 Agent
|
||||
history := session.GetMessages()
|
||||
events := runner.Run(ctx, history)
|
||||
content := collectAssistantFromEvents(events)
|
||||
|
||||
// 保存助手回复
|
||||
assistantMsg := schema.AssistantMessage(content, nil)
|
||||
if err := session.Append(assistantMsg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Session 与 Agent 的关系:业务层与框架层的协作
|
||||
|
||||
**关键理解:**
|
||||
|
||||
- **Session 是业务层概念**:由业务代码实现和管理,负责存储和加载对话历史
|
||||
- **Agent(Runner)是框架层概念**:由 Eino 框架提供,负责处理消息并生成回复
|
||||
- **两者的交互点**:业务层通过 `session.GetMessages()` 获取消息列表,传递给 `runner.Run(ctx, history)` 进行处理
|
||||
|
||||
**架构分层:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 业务层(你的代码) │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
||||
│ │ Session │───→│ GetMessages() │───→│ runner.Run() │ │
|
||||
│ │ (存储) │ │ (消息列表) │ │ (框架调用) │ │
|
||||
│ └─────────────┘ └──────────────┘ └───────────────┘ │
|
||||
│ ↑ │ │
|
||||
│ │ ↓ │
|
||||
│ ┌─────────────┐ ┌───────────────┐ │
|
||||
│ │ Append() │←─────────────────────│ 助手回复 │ │
|
||||
│ │ (保存消息) │ └───────────────┘ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 框架层(Eino 框架) │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ adk.Runner:接收消息列表,调用 ChatModel,返回回复 │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**流程图:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 用户输入 │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ session.Append() │
|
||||
│ 保存用户消息 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ session.GetMessages()│
|
||||
│ 获取完整历史 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ runner.Run(history) │
|
||||
│ Agent 处理消息 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 收集助手回复 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ session.Append() │
|
||||
│ 保存助手消息 │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
**框架层 vs 业务层:**
|
||||
|
||||
- **Eino 框架层**:提供 `adk.Runner`、`schema.Message` 等基础抽象,不关心消息如何存储
|
||||
- **业务层(本章实现)**:Memory/Session/Store 是业务层概念,用于管理对话历史的存储
|
||||
|
||||
**业务层概念:**
|
||||
|
||||
- **Memory**:对话历史的持久化存储,支持跨进程恢复
|
||||
- **Session**:一次完整的对话会话,包含 ID、创建时间、消息列表
|
||||
- **Store**:管理多个 Session 的存储,支持创建、获取、列表、删除
|
||||
- **JSONL 格式**:简单的文件格式,易于读写和扩展
|
||||
|
||||
**业务层与框架层的交互:**
|
||||
|
||||
- 业务层负责存储消息,通过 `session.GetMessages()` 获取消息列表
|
||||
- 将消息列表传递给框架层的 `runner.Run(ctx, history)` 进行处理
|
||||
- 收集框架层返回的回复,再由业务层保存到存储中
|
||||
|
||||
> **💡 提示**:本章的实现只是众多存储方案中的一种简单示例。在实际项目中,你可以根据业务需求选择数据库、Redis、云存储等方案,甚至可以实现更复杂的功能如会话过期清理、搜索、分享等。
|
||||
|
||||
## 扩展思考:业务层存储方案的选择
|
||||
|
||||
本章提供的 JSONL 文件存储方案适合简单的单机应用。在实际业务中,你可能需要考虑其他存储方案:
|
||||
|
||||
**其他存储实现:**
|
||||
|
||||
- 数据库存储(MySQL、PostgreSQL、MongoDB)
|
||||
- Redis 存储(支持分布式)
|
||||
- 云存储(S3、OSS)
|
||||
|
||||
**高级功能:**
|
||||
|
||||
- 会话过期清理
|
||||
- 会话搜索
|
||||
- 会话导出/导入
|
||||
- 会话分享
|
||||
@@ -0,0 +1,329 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-12"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第四章:Tool 与文件系统访问
|
||||
weight: 4
|
||||
---
|
||||
|
||||
本章目标:为 Agent 添加 Tool 能力,让 Agent 能够访问文件系统。
|
||||
|
||||
## 为什么需要 Tool
|
||||
|
||||
前三章我们实现的 Agent 只能对话,无法执行实际操作。
|
||||
|
||||
**Agent 的局限:**
|
||||
|
||||
- 只能生成文本回复
|
||||
- 无法访问外部资源(文件、API、数据库等)
|
||||
- 无法执行实际任务(计算、查询、修改等)
|
||||
|
||||
**Tool 的定位:**
|
||||
|
||||
- **Tool 是 Agent 的能力扩展**:让 Agent 能够执行具体操作
|
||||
- **Tool 封装了具体实现**:Agent 不关心 Tool 内部如何工作,只关心输入输出
|
||||
- **Tool 可组合**:一个 Agent 可以有多个 Tool,根据需要选择调用
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **Agent** = "智能助手"(能理解指令,但需要工具才能执行)
|
||||
- **Tool** = "工具箱"(文件操作、网络请求、数据库查询等)
|
||||
|
||||
## 为什么需要文件系统能力
|
||||
|
||||
本示例是 ChatWithDoc(与文档对话),目标是帮助用户学习 Eino 框架并编写 Eino 代码。那么,最好的文档是什么?
|
||||
|
||||
**答案就是:Eino 仓库的代码本身。**
|
||||
|
||||
- **Code**: 源代码展示了框架的真实实现
|
||||
- **Comment**: 代码注释提供了设计思路和使用说明
|
||||
- **Examples**: 示例代码演示了最佳实践
|
||||
|
||||
通过文件系统访问能力,Agent 可以直接读取 Eino 源码、注释和示例,为用户提供最准确、最及时的技术支持。
|
||||
|
||||
## 关键概念
|
||||
|
||||
### Tool 接口
|
||||
|
||||
`Tool` 是 Eino 中定义可执行能力的接口:
|
||||
|
||||
```go
|
||||
// BaseTool 提供工具的元信息,ChatModel 使用这些信息决定是否以及如何调用工具
|
||||
type BaseTool interface {
|
||||
Info(ctx context.Context) (*schema.ToolInfo, error)
|
||||
}
|
||||
|
||||
// InvokableTool 是可以被 ToolsNode 执行的工具
|
||||
type InvokableTool interface {
|
||||
BaseTool
|
||||
// InvokableRun 执行工具,参数是 JSON 编码的字符串,返回字符串结果
|
||||
InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)
|
||||
}
|
||||
|
||||
// StreamableTool 是 InvokableTool 的流式变体
|
||||
type StreamableTool interface {
|
||||
BaseTool
|
||||
// StreamableRun 流式执行工具,返回 StreamReader
|
||||
StreamableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (*schema.StreamReader[string], error)
|
||||
}
|
||||
```
|
||||
|
||||
**接口层次:**
|
||||
|
||||
- `BaseTool`:基础接口,只提供元信息
|
||||
- `InvokableTool`:可执行工具(继承 BaseTool)
|
||||
- `StreamableTool`:流式工具(继承 BaseTool)
|
||||
|
||||
### Backend 接口
|
||||
|
||||
`Backend` 是 Eino 中用于文件系统操作的抽象接口:
|
||||
|
||||
```go
|
||||
type Backend interface {
|
||||
// 列出目录下的文件信息
|
||||
LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error)
|
||||
|
||||
// 读取文件内容,支持按行偏移和限制
|
||||
Read(ctx context.Context, req *ReadRequest) (*FileContent, error)
|
||||
|
||||
// 在文件中搜索匹配的内容
|
||||
GrepRaw(ctx context.Context, req *GrepRequest) ([]GrepMatch, error)
|
||||
|
||||
// 根据 glob 模式匹配文件
|
||||
GlobInfo(ctx context.Context, req *GlobInfoRequest) ([]FileInfo, error)
|
||||
|
||||
// 写入文件内容
|
||||
Write(ctx context.Context, req *WriteRequest) error
|
||||
|
||||
// 编辑文件内容(字符串替换)
|
||||
Edit(ctx context.Context, req *EditRequest) error
|
||||
}
|
||||
```
|
||||
|
||||
### LocalBackend
|
||||
|
||||
`LocalBackend` 是 Backend 的本地文件系统实现,直接访问操作系统的文件系统:
|
||||
|
||||
```go
|
||||
import localbk "github.com/cloudwego/eino-ext/adk/backend/local"
|
||||
|
||||
backend, err := localbk.NewBackend(ctx, &localbk.Config{})
|
||||
```
|
||||
|
||||
**特点:**
|
||||
|
||||
- 直接访问本地文件系统,使用 Go 标准库实现
|
||||
- 支持所有 Backend 接口方法
|
||||
- 支持执行 shell 命令(ExecuteStreaming)
|
||||
- 路径安全:要求使用绝对路径,防止目录遍历攻击
|
||||
- 零配置:开箱即用,无需额外设置
|
||||
|
||||
## 实现:使用 DeepAgent
|
||||
|
||||
本章使用 DeepAgent 预构建 Agent,它提供了 Backend 和 StreamingShell 的一级配置,可以方便地注册文件系统相关的工具。
|
||||
|
||||
### 从 ChatModelAgent 到 DeepAgent:何时需要切换?
|
||||
|
||||
前面章节一直使用 `ChatModelAgent`,它已经能处理多轮对话。但要访问文件系统,我们需要切换到 `DeepAgent`。
|
||||
|
||||
**ChatModelAgent vs DeepAgent 对比:**
|
||||
|
||||
<table>
|
||||
<tr><td>能力</td><td>ChatModelAgent</td><td>DeepAgent</td></tr>
|
||||
<tr><td>多轮对话</td><td>✅</td><td>✅</td></tr>
|
||||
<tr><td>添加自定义 Tool</td><td>✅ 手动注册每个 Tool</td><td>✅ 手动注册或自动注册</td></tr>
|
||||
<tr><td>文件系统访问(Backend)</td><td>❌ 需手动创建并注册所有文件工具</td><td>✅ 一级配置,自动注册</td></tr>
|
||||
<tr><td>命令执行(StreamingShell)</td><td>❌ 需手动创建</td><td>✅ 一级配置,自动注册</td></tr>
|
||||
<tr><td>内置任务管理</td><td>❌</td><td>✅ <pre>write_todos</pre> 工具</td></tr>
|
||||
<tr><td>支持子 Agent</td><td>❌</td><td>✅</td></tr>
|
||||
</table>
|
||||
|
||||
**选择建议:**
|
||||
|
||||
- 纯对话场景(无外部访问)→ 用 `ChatModelAgent`
|
||||
- 需要访问文件系统或执行命令 → 用 `DeepAgent`
|
||||
|
||||
### 为什么使用 DeepAgent?
|
||||
|
||||
相比直接使用 ChatModelAgent,DeepAgent 的优势:
|
||||
|
||||
1. **一级配置**: Backend 和 StreamingShell 是一级配置,直接传入即可
|
||||
2. **自动注册工具**: 配置 Backend 后自动注册文件系统工具,无需手动创建
|
||||
3. **内置任务管理**: 提供 `write_todos` 工具,支持任务规划和跟踪
|
||||
4. **支持子 Agent**: 可以配置专门的子 Agent 处理特定任务
|
||||
5. **更强大**: 集成了文件系统、命令执行等多种能力
|
||||
|
||||
### 代码实现
|
||||
|
||||
```go
|
||||
import (
|
||||
localbk "github.com/cloudwego/eino-ext/adk/backend/local"
|
||||
"github.com/cloudwego/eino/adk/prebuilt/deep"
|
||||
)
|
||||
|
||||
// 创建 LocalBackend
|
||||
backend, err := localbk.NewBackend(ctx, &localbk.Config{})
|
||||
|
||||
// 创建 DeepAgent,自动注册文件系统工具
|
||||
agent, err := deep.New(ctx, &deep.Config{
|
||||
Name: "Ch04ToolAgent",
|
||||
Description: "ChatWithDoc agent with filesystem access via LocalBackend.",
|
||||
ChatModel: cm,
|
||||
Instruction: instruction,
|
||||
Backend: backend, // 提供文件系统操作能力
|
||||
StreamingShell: backend, // 提供命令执行能力
|
||||
MaxIteration: 50,
|
||||
})
|
||||
```
|
||||
|
||||
### DeepAgent 自动注册的工具
|
||||
|
||||
当配置了 `Backend` 和 `StreamingShell` 后,DeepAgent 会自动注册以下工具:
|
||||
|
||||
- `read_file`: 读取文件内容
|
||||
- `write_file`: 写入文件内容
|
||||
- `edit_file`: 编辑文件内容
|
||||
- `glob`: 根据 glob 模式查找文件
|
||||
- `grep`: 在文件中搜索内容
|
||||
- `execute`: 执行 shell 命令
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch04/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch04/main.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。
|
||||
|
||||
本章还需要设置 `PROJECT_ROOT`(可选,见下方运行说明)。
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
# 可选:设置 Eino 核心库的根目录路径
|
||||
# 未设置时,Agent 默认使用当前工作目录(即 chatwitheino 目录)作为根目录
|
||||
# 若要让 Agent 能检索完整的 Eino 代码库,建议指向 eino 核心库根目录
|
||||
export PROJECT_ROOT=/path/to/eino
|
||||
|
||||
# 验证路径是否正确(应该能看到 adk、components、compose 等目录)
|
||||
ls $PROJECT_ROOT
|
||||
|
||||
go run ./cmd/ch04
|
||||
```
|
||||
|
||||
**PROJECT_ROOT 说明:**
|
||||
|
||||
- **不设置时**:`PROJECT_ROOT` 默认为当前工作目录(`chatwitheino` 所在目录),Agent 只能访问本示例项目的文件。这对于快速试验已足够。
|
||||
- **设置后**:指向 Eino 核心库根目录,Agent 可以检索 Eino 框架的完整代码库(核心库、扩展库、示例库)。这是 ChatWithEino 的完整使用场景。
|
||||
|
||||
**推荐的三仓库目录结构(如要完整体验):**
|
||||
|
||||
```
|
||||
eino/ # PROJECT_ROOT(Eino 核心库)
|
||||
├── adk/
|
||||
├── components/
|
||||
├── compose/
|
||||
├── ext/ # eino-ext(扩展组件,如 OpenAI、Ark 等实现)
|
||||
├── examples/ # eino-examples(本仓库,本示例所在位置)
|
||||
│ └── quickstart/
|
||||
│ └── chatwitheino/
|
||||
└── ...
|
||||
```
|
||||
|
||||
可以使用 `dev_setup.sh` 脚本自动设置上述目录结构:
|
||||
|
||||
```bash
|
||||
# 在 eino 根目录运行,自动克隆扩展库和示例库到正确位置
|
||||
bash scripts/dev_setup.sh
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
you> 列出当前目录的文件
|
||||
[assistant] 我来帮你列出当前目录的文件...
|
||||
[tool call] glob(pattern: "*")
|
||||
[tool result] 找到 5 个文件:
|
||||
- main.go
|
||||
- go.mod
|
||||
- go.sum
|
||||
- README.md
|
||||
- cmd/
|
||||
|
||||
you> 读取 main.go 文件的内容
|
||||
[assistant] 我来读取 main.go 文件...
|
||||
[tool call] read_file(file_path: "main.go")
|
||||
[tool result] 文件内容如下:
|
||||
...
|
||||
```
|
||||
|
||||
**注意:** 如果在运行过程中遇到 Tool 报错导致 Agent 中断,请不要 panic,这是正常现象。Tool 报错是常见的情况,例如参数错误、文件不存在等。如何优雅地处理 Tool 错误,我们将在下一章详细介绍。
|
||||
|
||||
## Tool 调用流程
|
||||
|
||||
当 Agent 需要调用 Tool 时:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 用户:列出当前目录的文件 │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 分析意图 │
|
||||
│ 决定调用 glob 工具 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 生成 Tool Call │
|
||||
│ {"pattern": "*"} │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 执行 Tool │
|
||||
│ glob("*") │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 返回 Tool Result │
|
||||
│ {"files": [...]} │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 生成回复 │
|
||||
│ "找到 5 个文件..." │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Tool**:Agent 的能力扩展,让 Agent 能够执行具体操作
|
||||
- **Backend**:文件系统操作的抽象接口,提供统一的文件操作能力
|
||||
- **LocalBackend**:Backend 的本地文件系统实现,直接访问操作系统文件系统
|
||||
- **DeepAgent**:预构建的高级 Agent,提供 Backend 和 StreamingShell 的一级配置
|
||||
- **自动注册工具**:配置 Backend 后自动注册文件系统工具
|
||||
- **Tool 调用流程**:Agent 分析意图 → 生成 Tool Call → 执行 Tool → 返回结果 → 生成回复
|
||||
|
||||
## 扩展思考
|
||||
|
||||
**其他 Tool 类型:**
|
||||
|
||||
- HTTP Tool:调用外部 API
|
||||
- Database Tool:查询数据库
|
||||
- Calculator Tool:执行计算
|
||||
- Code Executor Tool:运行代码
|
||||
|
||||
**其他 Backend 实现:**
|
||||
|
||||
- 可以基于 Backend 接口实现其他存储后端
|
||||
- 例如:云存储、数据库存储等
|
||||
- LocalBackend 已经提供了完整的文件系统操作能力
|
||||
|
||||
**自定义 Tool 创建:**
|
||||
|
||||
如果需要创建自定义 Tool,可以使用 `utils.InferTool` 从函数自动推断。详见:
|
||||
|
||||
- [Tool 接口文档](https://github.com/cloudwego/eino/tree/main/components/tool)
|
||||
- [Tool 创建示例](https://github.com/cloudwego/eino-examples/tree/main/components/tool)
|
||||
@@ -0,0 +1,448 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-16"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第五章:Middleware(中间件模式)
|
||||
weight: 5
|
||||
---
|
||||
|
||||
本章目标:理解 Middleware 模式,实现 Tool 错误处理和 ChatModel 重试机制。
|
||||
|
||||
## 为什么需要 Middleware
|
||||
|
||||
第四章我们为 Agent 添加了 Tool 能力,让 Agent 能够访问文件系统。但在实际应用场景中,**Tool 报错或 ChatModel 报错是常见的现象**,例如:
|
||||
|
||||
- **Tool 报错**:文件不存在、参数错误、权限不足等
|
||||
- **ChatModel 报错**:API 限流(429)、网络超时、服务不可用等
|
||||
|
||||
### 问题一:Tool 错误会中断整个流程
|
||||
|
||||
当 Tool 执行失败时,错误会直接传播到 Agent,导致整个对话中断:
|
||||
|
||||
```
|
||||
[tool call] read_file(file_path: "nonexistent.txt")
|
||||
Error: open nonexistent.txt: no such file or directory
|
||||
// 对话中断,用户需要重新开始
|
||||
```
|
||||
|
||||
### 问题二:模型调用可能因限流失败
|
||||
|
||||
当模型 API 返回 429(Too Many Requests)错误时,整个对话也会中断:
|
||||
|
||||
```
|
||||
Error: rate limit exceeded (429)
|
||||
// 对话中断
|
||||
```
|
||||
|
||||
### 期望的行为
|
||||
|
||||
这些报错信息往往**不希望直接终止 Agent 流程**,而是希望把报错信息给到模型,由模型自动纠错进行下一轮。例如:
|
||||
|
||||
```
|
||||
[tool call] read_file(file_path: "nonexistent.txt")
|
||||
[tool result] [tool error] open nonexistent.txt: no such file or directory
|
||||
[assistant] 抱歉,文件不存在。让我先列出当前目录的文件...
|
||||
[tool call] glob(pattern: "*")
|
||||
```
|
||||
|
||||
### Middleware 的定位
|
||||
|
||||
**Middleware 模式**可以扩展 Tool 和 ChatModel 的行为,非常适合解决这个问题:
|
||||
|
||||
- **Middleware 是 Agent 的拦截器**:在调用前后插入自定义逻辑
|
||||
- **Middleware 可处理错误**:将错误转换为模型可理解的格式
|
||||
- **Middleware 可实现重试**:自动重试失败的操作
|
||||
- **Middleware 可组合**:多个 Middleware 可以串联使用
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **Agent** = "业务逻辑"
|
||||
- **Middleware** = "AOP 切面"(日志、重试、错误处理等横切关注点)
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch05/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch05/main.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。同时,需要与第四章一样设置 `PROJECT_ROOT`:
|
||||
|
||||
```bash
|
||||
export PROJECT_ROOT=/path/to/eino # Eino 核心库根目录
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
# 设置项目根目录
|
||||
export PROJECT_ROOT=/path/to/your/project
|
||||
|
||||
go run ./cmd/ch05
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
you> 列出当前目录的文件
|
||||
[assistant] 我来帮你列出文件...
|
||||
[tool call] list_files(directory: ".")
|
||||
|
||||
you> 读取一个不存在的文件
|
||||
[assistant] 尝试读取文件...
|
||||
[tool call] read_file(file_path: "nonexistent.txt")
|
||||
[tool result] [tool error] open nonexistent.txt: no such file or directory
|
||||
[assistant] 抱歉,文件不存在...
|
||||
```
|
||||
|
||||
## 关键概念
|
||||
|
||||
### Middleware 接口
|
||||
|
||||
`ChatModelAgentMiddleware` 是 Agent 的中间件接口:
|
||||
|
||||
```go
|
||||
type ChatModelAgentMiddleware interface {
|
||||
// BeforeAgent is called before each agent run, allowing modification of
|
||||
// the agent's instruction and tools configuration.
|
||||
BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
|
||||
|
||||
// BeforeModelRewriteState is called before each model invocation.
|
||||
// The returned state is persisted to the agent's internal state and passed to the model.
|
||||
BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
|
||||
|
||||
// AfterModelRewriteState is called after each model invocation.
|
||||
// The input state includes the model's response as the last message.
|
||||
AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
|
||||
|
||||
// WrapInvokableToolCall wraps a tool's synchronous execution with custom behavior.
|
||||
// This method is only called for tools that implement InvokableTool.
|
||||
WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error)
|
||||
|
||||
// WrapStreamableToolCall wraps a tool's streaming execution with custom behavior.
|
||||
// This method is only called for tools that implement StreamableTool.
|
||||
WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error)
|
||||
|
||||
// WrapEnhancedInvokableToolCall wraps an enhanced tool's synchronous execution.
|
||||
// This method is only called for tools that implement EnhancedInvokableTool.
|
||||
WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error)
|
||||
|
||||
// WrapEnhancedStreamableToolCall wraps an enhanced tool's streaming execution.
|
||||
// This method is only called for tools that implement EnhancedStreamableTool.
|
||||
WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error)
|
||||
|
||||
// WrapModel wraps a chat model with custom behavior.
|
||||
// This method is called at request time when the model is about to be invoked.
|
||||
WrapModel(ctx context.Context, m model.BaseChatModel, mc *ModelContext) (model.BaseChatModel, error)
|
||||
}
|
||||
```
|
||||
|
||||
**设计理念:**
|
||||
|
||||
- **装饰器模式**:每个 Middleware 包装原始调用,可以修改输入、输出或错误
|
||||
- **洋葱模型**:请求从外向内穿过 Middleware,响应从内向外返回
|
||||
- **可组合**:多个 Middleware 按顺序执行
|
||||
|
||||
### Middleware 执行顺序
|
||||
|
||||
`Handlers`(即 Middlewares)按**数组正序**包装,形成洋葱模型:
|
||||
|
||||
```go
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&middlewareA{}, // 最外层:最先 Wrap,最先拦截请求,但 WrapModel 最后生效
|
||||
&middlewareB{}, // 中间层
|
||||
&middlewareC{}, // 最内层:最后 Wrap
|
||||
}
|
||||
```
|
||||
|
||||
**对于 Tool 调用的执行顺序:**
|
||||
|
||||
```
|
||||
请求 → A.Wrap → B.Wrap → C.Wrap → 实际 Tool 执行 → C返回 → B返回 → A返回 → 响应
|
||||
```
|
||||
|
||||
**实用建议:** 将 `safeToolMiddleware`(错误捕获)放在最内层(数组末尾),确保其他 Middleware 抛出的中断错误能正确向外传播。
|
||||
|
||||
### SafeToolMiddleware
|
||||
|
||||
`SafeToolMiddleware` 将 Tool 错误转换为字符串,让模型能够理解并处理:
|
||||
|
||||
```go
|
||||
type safeToolMiddleware struct {
|
||||
*adk.BaseChatModelAgentMiddleware
|
||||
}
|
||||
|
||||
func (m *safeToolMiddleware) WrapInvokableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.InvokableToolCallEndpoint,
|
||||
_ *adk.ToolContext,
|
||||
) (adk.InvokableToolCallEndpoint, error) {
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
|
||||
result, err := endpoint(ctx, args, opts...)
|
||||
if err != nil {
|
||||
// 将错误转换为字符串,而不是返回错误
|
||||
return fmt.Sprintf("[tool error] %v", err), nil
|
||||
}
|
||||
return result, nil
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**效果:**
|
||||
|
||||
```
|
||||
[tool call] read_file(file_path: "nonexistent.txt")
|
||||
[tool result] [tool error] open nonexistent.txt: no such file or directory
|
||||
[assistant] 抱歉,文件不存在,请检查文件路径...
|
||||
// 对话继续,模型可以根据错误信息调整策略
|
||||
```
|
||||
|
||||
### ModelRetryConfig
|
||||
|
||||
`ModelRetryConfig` 配置 ChatModel 的自动重试:
|
||||
|
||||
```go
|
||||
type ModelRetryConfig struct {
|
||||
MaxRetries int // 最大重试次数
|
||||
IsRetryAble func(ctx context.Context, err error) bool // 判断是否可重试
|
||||
}
|
||||
```
|
||||
|
||||
**使用方式(以 DeepAgent 为例):**
|
||||
|
||||
```go
|
||||
agent, err := deep.New(ctx, &deep.Config{
|
||||
// ...
|
||||
ModelRetryConfig: &adk.ModelRetryConfig{
|
||||
MaxRetries: 5,
|
||||
IsRetryAble: func(_ context.Context, err error) bool {
|
||||
// 429 限流错误可重试
|
||||
return strings.Contains(err.Error(), "429") ||
|
||||
strings.Contains(err.Error(), "Too Many Requests") ||
|
||||
strings.Contains(err.Error(), "qpm limit")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**重试策略:**
|
||||
|
||||
- 指数退避:每次重试间隔递增
|
||||
- 可配置条件:通过 `IsRetryAble` 判断哪些错误可重试
|
||||
- 自动恢复:无需用户干预
|
||||
|
||||
## Middleware 的实现
|
||||
|
||||
### 1. 实现 SafeToolMiddleware
|
||||
|
||||
```go
|
||||
type safeToolMiddleware struct {
|
||||
*adk.BaseChatModelAgentMiddleware
|
||||
}
|
||||
|
||||
func (m *safeToolMiddleware) WrapInvokableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.InvokableToolCallEndpoint,
|
||||
_ *adk.ToolContext,
|
||||
) (adk.InvokableToolCallEndpoint, error) {
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
|
||||
result, err := endpoint(ctx, args, opts...)
|
||||
if err != nil {
|
||||
// 中断错误不转换,需要继续传播
|
||||
if _, ok := compose.IsInterruptRerunError(err); ok {
|
||||
return "", err
|
||||
}
|
||||
// 其他错误转换为字符串
|
||||
return fmt.Sprintf("[tool error] %v", err), nil
|
||||
}
|
||||
return result, nil
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实现流式 Tool 错误处理
|
||||
|
||||
```go
|
||||
func (m *safeToolMiddleware) WrapStreamableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.StreamableToolCallEndpoint,
|
||||
_ *adk.ToolContext,
|
||||
) (adk.StreamableToolCallEndpoint, error) {
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (*schema.StreamReader[string], error) {
|
||||
sr, err := endpoint(ctx, args, opts...)
|
||||
if err != nil {
|
||||
if _, ok := compose.IsInterruptRerunError(err); ok {
|
||||
return nil, err
|
||||
}
|
||||
// 返回包含错误信息的单帧流
|
||||
return singleChunkReader(fmt.Sprintf("[tool error] %v", err)), nil
|
||||
}
|
||||
// 包装流,捕获流中的错误
|
||||
return safeWrapReader(sr), nil
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置 Agent 使用 Middleware
|
||||
|
||||
本章继续使用第四章引入的 `DeepAgent`,在其 `Handlers` 字段中注册 Middleware:
|
||||
|
||||
```go
|
||||
agent, err := deep.New(ctx, &deep.Config{
|
||||
Name: "Ch05MiddlewareAgent",
|
||||
Description: "ChatWithDoc agent with safe tool middleware and retry.",
|
||||
ChatModel: cm,
|
||||
Instruction: agentInstruction,
|
||||
Backend: backend,
|
||||
StreamingShell: backend,
|
||||
MaxIteration: 50,
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&safeToolMiddleware{}, // 将 Tool 错误转换为字符串
|
||||
},
|
||||
ModelRetryConfig: &adk.ModelRetryConfig{
|
||||
MaxRetries: 5,
|
||||
IsRetryAble: func(_ context.Context, err error) bool {
|
||||
return strings.Contains(err.Error(), "429") ||
|
||||
strings.Contains(err.Error(), "Too Many Requests")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**注意**:`Handlers` 字段(在配置中)和 "Middleware"(在文档中讨论的概念)是同一回事——`Handlers` 是配置字段名,而 `ChatModelAgentMiddleware` 是接口名。
|
||||
|
||||
```
|
||||
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch05/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch05/main.go)):
|
||||
|
||||
```go
|
||||
// SafeToolMiddleware 捕获 Tool 错误并转换为字符串
|
||||
type safeToolMiddleware struct {
|
||||
*adk.BaseChatModelAgentMiddleware
|
||||
}
|
||||
|
||||
func (m *safeToolMiddleware) WrapInvokableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.InvokableToolCallEndpoint,
|
||||
_ *adk.ToolContext,
|
||||
) (adk.InvokableToolCallEndpoint, error) {
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
|
||||
result, err := endpoint(ctx, args, opts...)
|
||||
if err != nil {
|
||||
if _, ok := compose.IsInterruptRerunError(err); ok {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("[tool error] %v", err), nil
|
||||
}
|
||||
return result, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 配置 DeepAgent(与第四章一样,新增 Handlers 和 ModelRetryConfig)
|
||||
agent, _ := deep.New(ctx, &deep.Config{
|
||||
ChatModel: cm,
|
||||
Backend: backend,
|
||||
StreamingShell: backend,
|
||||
MaxIteration: 50,
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&safeToolMiddleware{},
|
||||
},
|
||||
ModelRetryConfig: &adk.ModelRetryConfig{
|
||||
MaxRetries: 5,
|
||||
IsRetryAble: func(_ context.Context, err error) bool {
|
||||
return strings.Contains(err.Error(), "429")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Middleware 执行流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 用户:读取不存在的文件 │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 分析意图 │
|
||||
│ 决定调用 read_file │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ SafeToolMiddleware │
|
||||
│ 拦截 Tool 调用 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 执行 read_file │
|
||||
│ 返回错误 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ SafeToolMiddleware │
|
||||
│ 将错误转换为字符串 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 返回 Tool Result │
|
||||
│ "[tool error] ..." │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 生成回复 │
|
||||
│ "抱歉,文件不存在..." │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Middleware**:Agent 的拦截器,可以在调用前后插入自定义逻辑
|
||||
- **SafeToolMiddleware**:将 Tool 错误转换为字符串,让模型能够理解并处理
|
||||
- **ModelRetryConfig**:配置 ChatModel 的自动重试,处理限流等临时错误
|
||||
- **装饰器模式**:Middleware 包装原始调用,可以修改输入、输出或错误
|
||||
- **洋葱模型**:请求从外向内穿过 Middleware,响应从内向外返回
|
||||
|
||||
## 扩展思考
|
||||
|
||||
**Eino 内置 Middleware:**
|
||||
|
||||
<table>
|
||||
<tr><td>Middleware</td><td>功能说明</td></tr>
|
||||
<tr><td><strong>reduction</strong></td><td>工具输出缩减,当工具返回内容过长时自动截断并卸载到文件系统,防止上下文溢出</td></tr>
|
||||
<tr><td><strong>summarization</strong></td><td>对话历史自动摘要,当 token 数量超过阈值时自动生成摘要压缩历史</td></tr>
|
||||
<tr><td><strong>skill</strong></td><td>技能加载中间件,让 Agent 能够动态加载和执行预定义的技能</td></tr>
|
||||
</table>
|
||||
|
||||
**Middleware 链示例:**
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/cloudwego/eino/adk/middlewares/reduction"
|
||||
"github.com/cloudwego/eino/adk/middlewares/summarization"
|
||||
"github.com/cloudwego/eino/adk/middlewares/skill"
|
||||
)
|
||||
|
||||
// 创建 reduction middleware:管理工具输出长度
|
||||
reductionMW, _ := reduction.New(ctx, &reduction.Config{
|
||||
Backend: filesystemBackend, // 存储后端
|
||||
MaxLengthForTrunc: 50000, // 单次工具输出最大长度
|
||||
MaxTokensForClear: 30000, // 触发清理的 token 阈值
|
||||
})
|
||||
|
||||
// 创建 summarization middleware:自动压缩对话历史
|
||||
summarizationMW, _ := summarization.New(ctx, &summarization.Config{
|
||||
Model: chatModel, // 用于生成摘要的模型
|
||||
Trigger: &summarization.TriggerCondition{
|
||||
ContextTokens: 190000, // 触发摘要的 token 阈值
|
||||
},
|
||||
})
|
||||
|
||||
// 组合多个 middleware(概念示例,使用 DeepAgent 时将 adk.NewChatModelAgent 替换为 deep.New)
|
||||
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||
Handlers: []adk.ChatModelAgentMiddleware{ // 注意:配置字段名为 Handlers,概念上与 Middlewares 等价
|
||||
summarizationMW, // 最外层:对话历史摘要
|
||||
reductionMW, // 中间层:工具输出缩减
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,367 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-12"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第六章:Callback 与 Trace(可观测性)
|
||||
weight: 6
|
||||
---
|
||||
|
||||
本章目标:理解 Callback 机制,集成 CozeLoop 实现链路追踪和可观测性。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch06/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch06/main.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。同时,需要与第四章一样设置 `PROJECT_ROOT`:
|
||||
|
||||
```bash
|
||||
export PROJECT_ROOT=/path/to/eino # Eino 核心库根目录(不设置则默认使用当前目录)
|
||||
```
|
||||
|
||||
可选:配置 CozeLoop 实现链路追踪:
|
||||
|
||||
```bash
|
||||
export COZELOOP_WORKSPACE_ID=your_workspace_id
|
||||
export COZELOOP_API_TOKEN=your_token
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
# 设置项目根目录
|
||||
export PROJECT_ROOT=/path/to/your/project
|
||||
|
||||
# 可选:配置 CozeLoop
|
||||
export COZELOOP_WORKSPACE_ID=your_workspace_id
|
||||
export COZELOOP_API_TOKEN=your_token
|
||||
|
||||
go run ./cmd/ch06
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
[trace] starting session: 083d16da-6b13-4fe6-afb0-c45d8f490ce1
|
||||
you> 你好
|
||||
[trace] chat_model_generate: model=gpt-4.1-mini tokens=150
|
||||
[trace] tool_call: name=list_files duration=23ms
|
||||
[assistant] 你好!有什么我可以帮助你的吗?
|
||||
```
|
||||
|
||||
## 从黑盒到白盒:为什么需要 Callback
|
||||
|
||||
前几章我们实现的 Agent 是一个"黑盒":输入问题,输出答案,但中间发生了什么我们并不清楚。
|
||||
|
||||
**黑盒的问题:**
|
||||
|
||||
- 不知道模型调用了多少次
|
||||
- 不知道 Tool 执行了多长时间
|
||||
- 不知道 Token 消耗了多少
|
||||
- 出问题时难以定位原因
|
||||
|
||||
**Callback 的定位:**
|
||||
|
||||
- **Callback 是 Eino 的旁路机制**:从 component 到 compose(下文详谈)到 adk,一以贯之
|
||||
- **Callback 在固定点位触发**:组件生命周期的 5 个关键时机
|
||||
- **Callback 可抽取实时信息**:输入、输出、错误、流式数据等
|
||||
- **Callback 用途广泛**:观测、日志、指标、追踪、调试、审计等
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **Agent** = "业务逻辑"(主路)
|
||||
- **Callback** = "旁路钩子"(在固定点位抽取信息)
|
||||
|
||||
## 关键概念
|
||||
|
||||
### Handler 接口
|
||||
|
||||
`Handler` 是 Eino 中定义回调处理器的核心接口:
|
||||
|
||||
```go
|
||||
type Handler interface {
|
||||
// 非流式输入(组件开始处理前)
|
||||
OnStart(ctx context.Context, info *RunInfo, input CallbackInput) context.Context
|
||||
|
||||
// 非流式输出(组件成功返回后)
|
||||
OnEnd(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context
|
||||
|
||||
// 错误(组件返回错误时)
|
||||
OnError(ctx context.Context, info *RunInfo, err error) context.Context
|
||||
|
||||
// 流式输入(组件接收流式输入时)
|
||||
OnStartWithStreamInput(ctx context.Context, info *RunInfo,
|
||||
input *schema.StreamReader[CallbackInput]) context.Context
|
||||
|
||||
// 流式输出(组件返回流式输出时)
|
||||
OnEndWithStreamOutput(ctx context.Context, info *RunInfo,
|
||||
output *schema.StreamReader[CallbackOutput]) context.Context
|
||||
}
|
||||
```
|
||||
|
||||
**设计理念:**
|
||||
|
||||
- **旁路机制**:不干扰主流程,在固定点位抽取信息
|
||||
- **全流程覆盖**:从 component 到 compose 到 adk,所有组件都支持
|
||||
- **状态传递**:同一 Handler 的 OnStart→OnEnd 可通过 context 传递状态
|
||||
- **性能优化**:实现 `TimingChecker` 接口可跳过不需要的时机
|
||||
|
||||
**RunInfo 结构:**
|
||||
|
||||
```go
|
||||
type RunInfo struct {
|
||||
Name string // 业务名称(节点名或用户指定)
|
||||
Type string // 实现类型(如 "OpenAI")
|
||||
Component string // 组件类型(如 "ChatModel")
|
||||
}
|
||||
```
|
||||
|
||||
**重要提示:**
|
||||
|
||||
- 流式回调必须关闭 StreamReader,否则会导致 goroutine 泄漏
|
||||
- 不要修改 Input/Output,它们被所有下游共享
|
||||
- RunInfo 可能为 nil,使用前需要检查
|
||||
|
||||
### CozeLoop
|
||||
|
||||
CozeLoop 是字节跳动开源的 AI 应用可观测性平台,提供了:
|
||||
|
||||
- **链路追踪**:完整的调用链路可视化
|
||||
- **指标监控**:延迟、Token 消耗、错误率等
|
||||
- **日志聚合**:集中管理所有日志
|
||||
- **调试支持**:在线查看和调试
|
||||
|
||||
**集成方式:**
|
||||
|
||||
```go
|
||||
import (
|
||||
clc "github.com/cloudwego/eino-ext/callbacks/cozeloop"
|
||||
"github.com/cloudwego/eino/callbacks"
|
||||
"github.com/coze-dev/cozeloop-go"
|
||||
)
|
||||
|
||||
// 创建 CozeLoop 客户端
|
||||
client, err := cozeloop.NewClient(
|
||||
cozeloop.WithAPIToken(apiToken),
|
||||
cozeloop.WithWorkspaceID(workspaceID),
|
||||
)
|
||||
|
||||
// 注册为全局 Callback
|
||||
callbacks.AppendGlobalHandlers(clc.NewLoopHandler(client))
|
||||
```
|
||||
|
||||
### Callback 的触发时机
|
||||
|
||||
Callback 在组件生命周期的 5 个关键时机触发。下表中 `Timing*` 是 Eino 内部常量名(用于 `TimingChecker` 接口),对应的 Handler 接口方法是右侧所示:
|
||||
|
||||
<table>
|
||||
<tr><td>时机常量</td><td>对应 Handler 方法</td><td>触发点</td><td>输入/输出</td></tr>
|
||||
<tr><td><pre>TimingOnStart</pre></td><td><pre>OnStart</pre></td><td>组件开始处理前</td><td>CallbackInput</td></tr>
|
||||
<tr><td><pre>TimingOnEnd</pre></td><td><pre>OnEnd</pre></td><td>组件成功返回后</td><td>CallbackOutput</td></tr>
|
||||
<tr><td><pre>TimingOnError</pre></td><td><pre>OnError</pre></td><td>组件返回错误时</td><td>error</td></tr>
|
||||
<tr><td><pre>TimingOnStartWithStreamInput</pre></td><td><pre>OnStartWithStreamInput</pre></td><td>组件接收流式输入时</td><td>StreamReader[CallbackInput]</td></tr>
|
||||
<tr><td><pre>TimingOnEndWithStreamOutput</pre></td><td><pre>OnEndWithStreamOutput</pre></td><td>组件返回流式输出时</td><td>StreamReader[CallbackOutput]</td></tr>
|
||||
</table>
|
||||
|
||||
**示例:ChatModel 调用流程**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ ChatModel.Generate(ctx, messages) │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ OnStart │ ← 输入: CallbackInput (messages)
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 模型处理 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ OnEnd │ ← 输出: CallbackOutput (response)
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
**示例:流式输出流程**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ ChatModel.Stream(ctx, messages) │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ OnStart │ ← 输入: CallbackInput (messages)
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 模型处理(流式) │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ OnEndWithStreamOutput │ ← 输出: StreamReader[CallbackOutput]
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 逐个 chunk 返回 │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
**注意:**
|
||||
|
||||
- 流式错误(stream 中途出错)不会触发 OnError,而是在 StreamReader 中返回
|
||||
- 同一 Handler 的 OnStart→OnEnd 可通过 context 传递状态
|
||||
- 不同 Handler 之间没有执行顺序保证
|
||||
|
||||
## Callback 的实现
|
||||
|
||||
### 1. 实现自定义 Callback Handler
|
||||
|
||||
完整实现 `Handler` 接口需要实现所有 5 个方法,较为繁琐。Eino 提供了 `callbacks.HandlerHelper` 帮助类来简化实现:
|
||||
|
||||
```go
|
||||
import "github.com/cloudwego/eino/callbacks"
|
||||
|
||||
// 使用 NewHandlerHelper 注册感兴趣的回调
|
||||
handler := callbacks.NewHandlerHelper().
|
||||
OnStart(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
|
||||
log.Printf("[trace] %s/%s start", info.Component, info.Name)
|
||||
return ctx
|
||||
}).
|
||||
OnEnd(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
|
||||
log.Printf("[trace] %s/%s end", info.Component, info.Name)
|
||||
return ctx
|
||||
}).
|
||||
OnError(func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
|
||||
log.Printf("[trace] %s/%s error: %v", info.Component, info.Name, err)
|
||||
return ctx
|
||||
}).
|
||||
Handler()
|
||||
|
||||
// 注册为全局 Callback
|
||||
callbacks.AppendGlobalHandlers(handler)
|
||||
```
|
||||
|
||||
**注意**:`RunInfo` 可能为 `nil`(如顶层调用没有 RunInfo),使用前请检查。
|
||||
|
||||
### 2. 集成 CozeLoop
|
||||
|
||||
```go
|
||||
func setupCozeLoop(ctx context.Context) (*cozeloop.Client, error) {
|
||||
apiToken := os.Getenv("COZELOOP_API_TOKEN")
|
||||
workspaceID := os.Getenv("COZELOOP_WORKSPACE_ID")
|
||||
|
||||
if apiToken == "" || workspaceID == "" {
|
||||
return nil, nil // 未配置则跳过
|
||||
}
|
||||
|
||||
client, err := cozeloop.NewClient(
|
||||
cozeloop.WithAPIToken(apiToken),
|
||||
cozeloop.WithWorkspaceID(workspaceID),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 注册为全局 Callback
|
||||
callbacks.AppendGlobalHandlers(clc.NewLoopHandler(client))
|
||||
|
||||
return client, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 在 main 中使用
|
||||
|
||||
```go
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// 设置 CozeLoop(可选)
|
||||
client, err := setupCozeLoop(ctx)
|
||||
if err != nil {
|
||||
log.Printf("cozeloop setup failed: %v", err)
|
||||
}
|
||||
if client != nil {
|
||||
defer func() {
|
||||
time.Sleep(5 * time.Second) // 等待数据上报
|
||||
client.Close(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
// 创建 Agent 并运行...
|
||||
}
|
||||
```
|
||||
|
||||
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch06/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch06/main.go)):
|
||||
|
||||
```go
|
||||
// 设置 CozeLoop 追踪
|
||||
cozeloopApiToken := os.Getenv("COZELOOP_API_TOKEN")
|
||||
cozeloopWorkspaceID := os.Getenv("COZELOOP_WORKSPACE_ID")
|
||||
if cozeloopApiToken != "" && cozeloopWorkspaceID != "" {
|
||||
client, err := cozeloop.NewClient(
|
||||
cozeloop.WithAPIToken(cozeloopApiToken),
|
||||
cozeloop.WithWorkspaceID(cozeloopWorkspaceID),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("cozeloop.NewClient failed: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
client.Close(ctx)
|
||||
}()
|
||||
callbacks.AppendGlobalHandlers(clc.NewLoopHandler(client))
|
||||
}
|
||||
```
|
||||
|
||||
## 可观测性的价值
|
||||
|
||||
### 1. 性能分析
|
||||
|
||||
通过 Callback 收集的数据,可以分析:
|
||||
|
||||
- 模型调用延迟分布
|
||||
- Tool 执行时间排行
|
||||
- Token 消耗趋势
|
||||
|
||||
### 2. 错误追踪
|
||||
|
||||
当 Agent 出现问题时:
|
||||
|
||||
- 查看完整的调用链路
|
||||
- 定位是哪个环节出错
|
||||
- 分析错误原因
|
||||
|
||||
### 3. 成本优化
|
||||
|
||||
通过 Token 消耗数据:
|
||||
|
||||
- 识别高消耗的对话
|
||||
- 优化 Prompt 减少 Token
|
||||
- 选择更经济的模型
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Callback**:Eino 的观测钩子,在关键节点触发回调
|
||||
- **CozeLoop**:字节跳动的 AI 应用可观测性平台
|
||||
- **全局注册**:通过 `callbacks.AppendGlobalHandlers` 注册全局 Callback
|
||||
- **非侵入式**:业务代码不需要修改,Callback 自动触发
|
||||
- **可观测性价值**:性能分析、错误追踪、成本优化
|
||||
|
||||
## 扩展思考
|
||||
|
||||
**其他 Callback 实现:**
|
||||
|
||||
- OpenTelemetry Callback:对接标准可观测性协议
|
||||
- 自定义日志 Callback:记录到本地文件
|
||||
- 指标 Callback:对接 Prometheus 等监控系统
|
||||
|
||||
**高级用法:**
|
||||
|
||||
- 在 Callback 中实现采样(只记录部分请求)
|
||||
- 在 Callback 中实现限流(根据 Token 消耗)
|
||||
- 在 Callback 中实现告警(错误率过高时通知)
|
||||
@@ -0,0 +1,355 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-16"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第七章:Interrupt/Resume(中断与恢复)
|
||||
weight: 7
|
||||
---
|
||||
|
||||
本章目标:理解 Interrupt/Resume 机制,实现 Tool 审批流程,让用户在敏感操作前进行确认。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch07/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch07/main.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。同时,需要与第四章一样设置 `PROJECT_ROOT`:
|
||||
|
||||
```bash
|
||||
export PROJECT_ROOT=/path/to/eino # Eino 核心库根目录(不设置则默认使用当前目录)
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
# 设置项目根目录
|
||||
export PROJECT_ROOT=/path/to/your/project
|
||||
|
||||
go run ./cmd/ch07
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
you> 请执行命令 echo hello
|
||||
|
||||
⚠️ Approval Required ⚠️
|
||||
Tool: execute
|
||||
Arguments: {"command":"echo hello"}
|
||||
|
||||
Approve this action? (y/n): y
|
||||
[tool result] hello
|
||||
|
||||
hello
|
||||
```
|
||||
|
||||
## 从自动执行到人工审批:为什么需要 Interrupt
|
||||
|
||||
前几章我们实现的 Agent 会自动执行所有 Tool 调用,但在某些场景下这是危险的:
|
||||
|
||||
**自动执行的风险:**
|
||||
|
||||
- 删除文件:误删重要数据
|
||||
- 发送邮件:发送错误内容
|
||||
- 执行命令:执行危险操作
|
||||
- 修改配置:破坏系统设置
|
||||
|
||||
**Interrupt 的定位:**
|
||||
|
||||
- **Interrupt 是 Agent 的暂停机制**:在关键操作前暂停,等待用户确认
|
||||
- **Interrupt 可携带信息**:向用户展示即将执行的操作
|
||||
- **Interrupt 可恢复**:用户确认后继续执行,拒绝后返回错误
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **自动执行** = "自动驾驶"(完全信任系统)
|
||||
- **Interrupt** = "人工接管"(关键决策由人来做)
|
||||
|
||||
## 关键概念
|
||||
|
||||
### Interrupt 机制
|
||||
|
||||
`Interrupt` 是 Eino 中实现人机协作的核心机制。
|
||||
|
||||
**核心思想:在执行关键操作前暂停,等待用户确认后继续。**
|
||||
|
||||
一个需要审批的 Tool 的执行被分成**两个阶段**:
|
||||
|
||||
1. **第一次调用(触发中断)**:Tool 保存当前参数,然后返回一个中断信号。Runner 暂停执行,向调用侧返回 Interrupt 事件。
|
||||
2. **用户审批后恢复(Resume)**:Runner 重新调用 Tool,此时 Tool 检测到"已中断过",直接读取用户的审批结果并执行(或拒绝)。
|
||||
|
||||
**简化版伪代码:**
|
||||
|
||||
```
|
||||
func myTool(ctx, args):
|
||||
if 第一次调用:
|
||||
保存 args
|
||||
return 中断信号 // Runner 暂停,展示审批提示
|
||||
else: // Resume 后的第二次调用
|
||||
if 用户批准:
|
||||
return 执行操作(保存的 args)
|
||||
else:
|
||||
return "操作被用户拒绝"
|
||||
```
|
||||
|
||||
**完整代码及关键字段说明:**
|
||||
|
||||
```go
|
||||
// 在 Tool 中触发中断
|
||||
func myTool(ctx context.Context, args string) (string, error) {
|
||||
// wasInterrupted: 是否是 Resume 后的第二次调用(第一次为 false,Resume 后为 true)
|
||||
// storedArgs: 第一次调用时通过 StatefulInterrupt 保存的参数,Resume 后可取回
|
||||
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
|
||||
|
||||
if !wasInterrupted {
|
||||
// 第一次调用:触发中断,同时保存 args 供 Resume 后使用
|
||||
return "", tool.StatefulInterrupt(ctx, &ApprovalInfo{
|
||||
ToolName: "my_tool",
|
||||
ArgumentsInJSON: args,
|
||||
}, args) // 第三个参数是要保存的状态(Resume 后通过 storedArgs 取回)
|
||||
}
|
||||
|
||||
// Resume 后的第二次调用:读取用户审批结果
|
||||
// isTarget: 本次 Resume 是否针对当前 Tool(一次 Resume 只针对一个 Tool)
|
||||
// hasData: Resume 时是否携带了审批结果数据
|
||||
// data: 用户传入的审批结果
|
||||
isTarget, hasData, data := tool.GetResumeContext[*ApprovalResult](ctx)
|
||||
if isTarget && hasData {
|
||||
if data.Approved {
|
||||
return doSomething(storedArgs) // 使用保存的参数执行实际操作
|
||||
}
|
||||
return "Operation rejected by user", nil
|
||||
}
|
||||
|
||||
// 其他情况(isTarget=false 意味着本次 Resume 目标不是当前 Tool):重新中断
|
||||
return "", tool.StatefulInterrupt(ctx, &ApprovalInfo{
|
||||
ToolName: "my_tool",
|
||||
ArgumentsInJSON: storedArgs,
|
||||
}, storedArgs)
|
||||
}
|
||||
```
|
||||
|
||||
### ApprovalMiddleware
|
||||
|
||||
`ApprovalMiddleware` 是一个通用的审批中间件,可以拦截特定 Tool 的调用:
|
||||
|
||||
```go
|
||||
type approvalMiddleware struct {
|
||||
*adk.BaseChatModelAgentMiddleware
|
||||
}
|
||||
|
||||
func (m *approvalMiddleware) WrapInvokableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.InvokableToolCallEndpoint,
|
||||
tCtx *adk.ToolContext,
|
||||
) (adk.InvokableToolCallEndpoint, error) {
|
||||
// 只拦截需要审批的 Tool
|
||||
if tCtx.Name != "execute" {
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
|
||||
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
|
||||
|
||||
if !wasInterrupted {
|
||||
return "", tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
|
||||
ToolName: tCtx.Name,
|
||||
ArgumentsInJSON: args,
|
||||
}, args)
|
||||
}
|
||||
|
||||
isTarget, hasData, data := tool.GetResumeContext[*commontool.ApprovalResult](ctx)
|
||||
if isTarget && hasData {
|
||||
if data.Approved {
|
||||
return endpoint(ctx, storedArgs, opts...)
|
||||
}
|
||||
if data.DisapproveReason != nil {
|
||||
return fmt.Sprintf("tool '%s' disapproved: %s", tCtx.Name, *data.DisapproveReason), nil
|
||||
}
|
||||
return fmt.Sprintf("tool '%s' disapproved", tCtx.Name), nil
|
||||
}
|
||||
|
||||
// 重新中断
|
||||
return "", tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
|
||||
ToolName: tCtx.Name,
|
||||
ArgumentsInJSON: storedArgs,
|
||||
}, storedArgs)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *approvalMiddleware) WrapStreamableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.StreamableToolCallEndpoint,
|
||||
tCtx *adk.ToolContext,
|
||||
) (adk.StreamableToolCallEndpoint, error) {
|
||||
// 如果 agent 配置了 StreamingShell,则 execute 会走流式调用,需要实现该方法才能拦截到
|
||||
if tCtx.Name != "execute" {
|
||||
return endpoint, nil
|
||||
}
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (*schema.StreamReader[string], error) {
|
||||
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
|
||||
if !wasInterrupted {
|
||||
return nil, tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
|
||||
ToolName: tCtx.Name,
|
||||
ArgumentsInJSON: args,
|
||||
}, args)
|
||||
}
|
||||
|
||||
isTarget, hasData, data := tool.GetResumeContext[*commontool.ApprovalResult](ctx)
|
||||
if isTarget && hasData {
|
||||
if data.Approved {
|
||||
return endpoint(ctx, storedArgs, opts...)
|
||||
}
|
||||
if data.DisapproveReason != nil {
|
||||
return singleChunkReader(fmt.Sprintf("tool '%s' disapproved: %s", tCtx.Name, *data.DisapproveReason)), nil
|
||||
}
|
||||
return singleChunkReader(fmt.Sprintf("tool '%s' disapproved", tCtx.Name)), nil
|
||||
}
|
||||
|
||||
isTarget, _, _ = tool.GetResumeContext[any](ctx)
|
||||
if !isTarget {
|
||||
return nil, tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
|
||||
ToolName: tCtx.Name,
|
||||
ArgumentsInJSON: storedArgs,
|
||||
}, storedArgs)
|
||||
}
|
||||
|
||||
return endpoint(ctx, storedArgs, opts...)
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### CheckPointStore
|
||||
|
||||
`CheckPointStore` 是实现中断恢复的关键组件:
|
||||
|
||||
```go
|
||||
type CheckPointStore interface {
|
||||
// 保存检查点
|
||||
Put(ctx context.Context, key string, checkpoint *Checkpoint) error
|
||||
|
||||
// 获取检查点
|
||||
Get(ctx context.Context, key string) (*Checkpoint, error)
|
||||
}
|
||||
```
|
||||
|
||||
**为什么需要 CheckPointStore?**
|
||||
|
||||
- 中断时保存状态:Tool 参数、执行位置等
|
||||
- 恢复时加载状态:从中断点继续执行
|
||||
- 支持跨进程恢复:进程重启后仍可恢复
|
||||
|
||||
## Interrupt/Resume 的实现
|
||||
|
||||
### 1. 配置 Runner 使用 CheckPointStore
|
||||
|
||||
```go
|
||||
runner := adk.NewRunner(ctx, adk.RunnerConfig{
|
||||
Agent: agent,
|
||||
EnableStreaming: true,
|
||||
CheckPointStore: adkstore.NewInMemoryStore(), // 内存存储
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 配置 Agent 使用 ApprovalMiddleware
|
||||
|
||||
```go
|
||||
agent, err := deep.New(ctx, &deep.Config{
|
||||
// ... 其他配置
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&approvalMiddleware{}, // 添加审批中间件
|
||||
&safeToolMiddleware{}, // 将 Tool 错误转换为字符串(中断类错误会继续向上抛出)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 处理中断事件
|
||||
|
||||
```go
|
||||
checkPointID := sessionID
|
||||
|
||||
events := runner.Run(ctx, history, adk.WithCheckPointID(checkPointID))
|
||||
content, interruptInfo, err := printAndCollectAssistantFromEvents(events)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if interruptInfo != nil {
|
||||
// 注意:建议使用同一个 stdin reader 同时读取「用户输入」与「审批 y/n」
|
||||
// 避免审批输入被当成下一轮 you> 的消息
|
||||
content, err = handleInterrupt(ctx, runner, checkPointID, interruptInfo, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = session.Append(schema.AssistantMessage(content, nil))
|
||||
```
|
||||
|
||||
## Interrupt/Resume 执行流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 用户:执行命令 echo hello │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 分析意图 │
|
||||
│ 决定调用 execute │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ ApprovalMiddleware │
|
||||
│ 拦截 Tool 调用 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 触发 Interrupt │
|
||||
│ 保存状态到 Store │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 返回 Interrupt 事件 │
|
||||
│ 等待用户审批 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 用户输入 y/n │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ runner.ResumeWith... │
|
||||
│ 恢复执行 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 执行 execute │
|
||||
│ 或返回拒绝信息 │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Interrupt**:Agent 的暂停机制,在关键操作前暂停等待确认
|
||||
- **Resume**:恢复执行,用户确认后继续或拒绝后返回错误
|
||||
- **ApprovalMiddleware**:通用审批中间件,拦截特定 Tool 调用
|
||||
- **CheckPointStore**:保存中断状态,支持跨进程恢复
|
||||
- **人机协作**:关键决策由人来确认,提高安全性
|
||||
|
||||
## 扩展思考
|
||||
|
||||
**其他 Interrupt 场景:**
|
||||
|
||||
- 多选项审批:用户选择多个选项之一
|
||||
- 参数补全:用户提供缺失的参数
|
||||
- 条件分支:用户决定执行路径
|
||||
|
||||
**审批策略:**
|
||||
|
||||
- 白名单:只审批敏感操作
|
||||
- 黑名单:审批所有操作,除了安全的
|
||||
- 动态规则:根据参数内容决定是否审批
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-12"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第八章:Graph Tool(复杂工作流)
|
||||
weight: 8
|
||||
---
|
||||
|
||||
本章目标:理解 Graph Tool 的概念,实现大文件的并行 chunk 召回,引入 compose 包构建复杂工作流。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch08/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch08/main.go)
|
||||
- RAG 实现:[rag/rag.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/rag/rag.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。
|
||||
|
||||
## 运行
|
||||
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
# 设置项目根目录
|
||||
export PROJECT_ROOT=/path/to/your/project
|
||||
|
||||
go run ./cmd/ch08
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
you> 请帮我分析 RFC6455 文档中关于 WebSocket 握手的部分
|
||||
[assistant] 我来帮你分析文档...
|
||||
[tool call] answer_from_document(file_path: "rfc6455.txt", question: "WebSocket 握手过程")
|
||||
[tool result] 找到 3 个相关片段,正在生成答案...
|
||||
[assistant] 根据 RFC6455 文档,WebSocket 握手过程如下...
|
||||
```
|
||||
|
||||
## 从简单 Tool 到 Graph Tool:为什么需要复杂工作流
|
||||
|
||||
第四章我们创建了简单的 Tool,每个 Tool 执行单一任务。但实际场景中,很多任务需要多个步骤协同完成。
|
||||
|
||||
**简单 Tool 的局限:**
|
||||
|
||||
- 单一职责:每个 Tool 只做一件事
|
||||
- 无法并行:多个独立任务无法同时执行
|
||||
- 难以复用:复杂逻辑难以拆分和组合
|
||||
|
||||
**重要说明:本章只是展示 compose/graph/workflow 能力的一角。**
|
||||
|
||||
从更大的视角看,Eino 的 `compose` 包提供了非常通用、确定性的编排能力:你可以把任何需要"确定性业务流程"的系统,用 `compose` 的 Graph/Chain/Workflow 组织成可执行的流水线,并且它能够**原生编排 Eino 的所有 component**(如 ChatModel、Prompt、Tools、Retriever、Embedding、Indexer 等),同时具备完整的 **callback** 体系,以及 **interrupt/resume + checkpoint** 支持。
|
||||
|
||||
**Graph Tool 的定位:**
|
||||
|
||||
- **Graph Tool 是 compose 工作流的 Tool 化封装**:把 `compose.Graph / compose.Chain / compose.Workflow` 这类可编译的编排产物,包装成一个 Agent 可调用的 Tool
|
||||
- **支持并行/分支/组合**:由 compose 提供(并行、分支、字段映射、子图等),Graph Tool 只是把它们暴露为 Tool 入口
|
||||
- **支持状态管理与持久化**:节点间传递数据、以及通过 checkpoint 保存/恢复运行状态
|
||||
- **可中断恢复**:既支持工作流内部的中断(节点里触发 interrupt),也支持工具层面的中断包装(嵌套 interrupt 场景)
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **简单 Tool** = "单步操作"(读取文件)
|
||||
- **Graph Tool** = "流水线"(读取 → 分块 → 评分 → 筛选 → 生成答案)
|
||||
|
||||
## 关键概念
|
||||
|
||||
### compose.Workflow
|
||||
|
||||
`compose.Workflow` 是 Eino 中构建工作流的核心组件:
|
||||
|
||||
```go
|
||||
wf := compose.NewWorkflow[Input, Output]()
|
||||
|
||||
// 添加节点
|
||||
wf.AddLambdaNode("load", loadFunc).AddInput(compose.START)
|
||||
wf.AddLambdaNode("chunk", chunkFunc).AddInput("load")
|
||||
wf.AddLambdaNode("score", scoreFunc).AddInput("chunk")
|
||||
wf.AddLambdaNode("answer", answerFunc).AddInput("score")
|
||||
|
||||
// 连接到结束节点
|
||||
wf.End().AddInput("answer")
|
||||
```
|
||||
|
||||
**核心概念:**
|
||||
|
||||
- **Node**:工作流中的处理单元
|
||||
- **Edge**:节点间的数据流向
|
||||
- **START**:工作流的入口
|
||||
- **END**:工作流的出口
|
||||
|
||||
### BatchNode
|
||||
|
||||
`BatchNode` 用于并行处理多个任务:
|
||||
|
||||
```go
|
||||
scorer := batch.NewBatchNode(&batch.NodeConfig[Task, Result]{
|
||||
Name: "ChunkScorer",
|
||||
InnerTask: scoreOneChunk, // 单个任务的处理函数
|
||||
MaxConcurrency: 5, // 最大并发数
|
||||
})
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
|
||||
1. 接收任务列表作为输入
|
||||
2. 并行执行每个任务(受 MaxConcurrency 限制)
|
||||
3. 收集所有结果返回
|
||||
|
||||
### FieldMapping
|
||||
|
||||
`FieldMapping` 用于跨节点传递数据:
|
||||
|
||||
```go
|
||||
wf.AddLambdaNode("answer", answerFunc).
|
||||
AddInputWithOptions("filter", // 从 filter 节点获取数据
|
||||
[]*compose.FieldMapping{compose.ToField("TopK")},
|
||||
compose.WithNoDirectDependency()).
|
||||
AddInputWithOptions(compose.START, // 从 START 节点获取数据
|
||||
[]*compose.FieldMapping{compose.MapFields("Question", "Question")},
|
||||
compose.WithNoDirectDependency())
|
||||
```
|
||||
|
||||
**为什么需要 FieldMapping?**
|
||||
|
||||
- 非相邻节点间传递数据
|
||||
- 多个数据源合并到同一节点
|
||||
- 数据字段重命名
|
||||
|
||||
## Graph Tool 的实现
|
||||
|
||||
### 1. 定义输入输出结构
|
||||
|
||||
```go
|
||||
type Input struct {
|
||||
FilePath string `json:"file_path" jsonschema:"description=Absolute path to the document"`
|
||||
Question string `json:"question" jsonschema:"description=The question to answer"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Answer string `json:"answer"`
|
||||
Sources []string `json:"sources"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 构建工作流
|
||||
|
||||
```go
|
||||
func buildWorkflow(cm model.BaseChatModel) *compose.Workflow[Input, Output] {
|
||||
wf := compose.NewWorkflow[Input, Output]()
|
||||
|
||||
// load: 读取文件
|
||||
wf.AddLambdaNode("load", compose.InvokableLambda(
|
||||
func(ctx context.Context, in Input) ([]*schema.Document, error) {
|
||||
data, err := os.ReadFile(in.FilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*schema.Document{{Content: string(data)}}, nil
|
||||
},
|
||||
)).AddInput(compose.START)
|
||||
|
||||
// chunk: 分块
|
||||
wf.AddLambdaNode("chunk", compose.InvokableLambda(
|
||||
func(ctx context.Context, docs []*schema.Document) ([]*schema.Document, error) {
|
||||
var out []*schema.Document
|
||||
for _, d := range docs {
|
||||
out = append(out, splitIntoChunks(d.Content, 800)...)
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
)).AddInput("load")
|
||||
|
||||
// score: 并行评分
|
||||
scorer := batch.NewBatchNode(&batch.NodeConfig[scoreTask, scoredChunk]{
|
||||
Name: "ChunkScorer",
|
||||
InnerTask: newScoreWorkflow(cm),
|
||||
MaxConcurrency: 5,
|
||||
})
|
||||
|
||||
wf.AddLambdaNode("score", compose.InvokableLambda(
|
||||
func(ctx context.Context, in scoreIn) ([]scoredChunk, error) {
|
||||
tasks := make([]scoreTask, len(in.Chunks))
|
||||
for i, c := range in.Chunks {
|
||||
tasks[i] = scoreTask{Text: c.Content, Question: in.Question}
|
||||
}
|
||||
return scorer.Invoke(ctx, tasks)
|
||||
},
|
||||
)).
|
||||
AddInputWithOptions("chunk", []*compose.FieldMapping{compose.ToField("Chunks")}, compose.WithNoDirectDependency()).
|
||||
AddInputWithOptions(compose.START, []*compose.FieldMapping{compose.MapFields("Question", "Question")}, compose.WithNoDirectDependency())
|
||||
|
||||
// filter: 筛选 top-k
|
||||
wf.AddLambdaNode("filter", compose.InvokableLambda(
|
||||
func(ctx context.Context, scored []scoredChunk) ([]scoredChunk, error) {
|
||||
sort.Slice(scored, func(i, j int) bool {
|
||||
return scored[i].Score > scored[j].Score
|
||||
})
|
||||
// 返回 top-3
|
||||
if len(scored) > 3 {
|
||||
scored = scored[:3]
|
||||
}
|
||||
return scored, nil
|
||||
},
|
||||
)).AddInput("score")
|
||||
|
||||
// answer: 生成答案
|
||||
wf.AddLambdaNode("answer", compose.InvokableLambda(
|
||||
func(ctx context.Context, in synthIn) (Output, error) {
|
||||
return synthesize(ctx, cm, in)
|
||||
},
|
||||
)).
|
||||
AddInputWithOptions("filter", []*compose.FieldMapping{compose.ToField("TopK")}, compose.WithNoDirectDependency()).
|
||||
AddInputWithOptions(compose.START, []*compose.FieldMapping{compose.MapFields("Question", "Question")}, compose.WithNoDirectDependency())
|
||||
|
||||
wf.End().AddInput("answer")
|
||||
|
||||
return wf
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 封装为 Tool
|
||||
|
||||
```go
|
||||
func BuildTool(ctx context.Context, cm model.BaseChatModel) (tool.BaseTool, error) {
|
||||
wf := buildWorkflow(cm)
|
||||
return graphtool.NewInvokableGraphTool[Input, Output](
|
||||
wf,
|
||||
"answer_from_document",
|
||||
"Search a large document for relevant content and synthesize an answer.",
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [rag/rag.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/rag/rag.go)):
|
||||
|
||||
```go
|
||||
// 构建工作流
|
||||
wf := compose.NewWorkflow[Input, Output]()
|
||||
|
||||
// 添加节点
|
||||
wf.AddLambdaNode("load", loadFunc).AddInput(compose.START)
|
||||
wf.AddLambdaNode("chunk", chunkFunc).AddInput("load")
|
||||
wf.AddLambdaNode("score", scoreFunc).
|
||||
AddInputWithOptions("chunk", []*compose.FieldMapping{compose.ToField("Chunks")}, compose.WithNoDirectDependency()).
|
||||
AddInputWithOptions(compose.START, []*compose.FieldMapping{compose.MapFields("Question", "Question")}, compose.WithNoDirectDependency())
|
||||
|
||||
// 封装为 Tool
|
||||
return graphtool.NewInvokableGraphTool[Input, Output](wf, "answer_from_document", "...")
|
||||
```
|
||||
|
||||
## Graph Tool 执行流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 输入:file_path, question │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ load: 读取文件 │
|
||||
│ 输出: []*Document │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ chunk: 分块 │
|
||||
│ 输出: []*Document │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ score: 并行评分 │
|
||||
│ (MaxConcurrency=5) │
|
||||
│ 输出: []scoredChunk │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ filter: 筛选 top-k │
|
||||
│ 输出: []scoredChunk │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ answer: 生成答案 │
|
||||
│ 输出: Output │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 返回结果 │
|
||||
│ {answer, sources} │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Graph Tool**:将复杂工作流封装为 Tool,支持多步骤协同
|
||||
- **compose.Workflow**:构建工作流的核心组件
|
||||
- **BatchNode**:并行处理多个任务
|
||||
- **FieldMapping**:跨节点传递数据
|
||||
- **可中断恢复**:Graph Tool 支持 Checkpoint 机制
|
||||
|
||||
## 扩展思考
|
||||
|
||||
**其他 Graph Tool 应用:**
|
||||
|
||||
- 多文档 RAG:并行处理多个文档
|
||||
- 多模型协作:不同模型处理不同任务
|
||||
- 复杂决策树:根据条件选择不同分支
|
||||
|
||||
**性能优化:**
|
||||
|
||||
- 调整 MaxConcurrency 控制并发
|
||||
- 使用缓存避免重复计算
|
||||
- 流式输出提升用户体验
|
||||
@@ -0,0 +1,252 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-16"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第十章:A2UI 协议(流式 UI 组件)
|
||||
weight: 10
|
||||
---
|
||||
|
||||
本章目标:实现 A2UI 协议,将 Agent 的输出渲染为流式 UI 组件。
|
||||
|
||||
## 重要说明:A2UI 的边界
|
||||
|
||||
A2UI 并不属于 Eino 框架本身的范畴,它是一个业务层的 UI 协议/渲染方案。本章把 A2UI 集成进前面章节逐步构建出来的 Agent,是为了提供一个端到端、可落地的完整示例:从模型调用、工具调用、工作流编排,到最终把结果以更友好的 UI 方式呈现出来。
|
||||
|
||||
在真实业务场景中,你完全可以根据产品形态选择不同的 UI 形式,例如:
|
||||
|
||||
- Web / App:自定义组件、表格、卡片、图表等
|
||||
- IM/办公套件:消息卡片、交互式表单
|
||||
- 命令行:纯文本或 TUI(终端 UI)
|
||||
|
||||
Eino 更关注“可组合的智能执行与编排能力”,至于“如何呈现给用户”,属于业务层可以自由扩展的一环。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/main.go)
|
||||
- Agent 构建:[agent.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/agent.go)
|
||||
- 服务端路由:[server/server.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/server/server.go)
|
||||
- A2UI 子集实现:[a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go)
|
||||
- A2UI 事件流转换:[a2ui/streamer.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/streamer.go)
|
||||
- 前端页面:[static/index.html](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/static/index.html)
|
||||
|
||||
## 前置条件
|
||||
|
||||
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)
|
||||
|
||||
## 运行
|
||||
|
||||
在 `quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
starting server on http://localhost:8080
|
||||
```
|
||||
|
||||
### (可选)启用 ch09 的 skills 能力
|
||||
|
||||
最终 Web 版使用的 Agent 构建逻辑与 Chapter 9 对齐:当 `EINO_EXT_SKILLS_DIR` 指向一个合法 skills 目录时,会自动注册 `skill` 中间件,模型就能按需调用 `skill` 工具加载 `eino-guide` / `eino-component` / `eino-compose` / `eino-agent`。
|
||||
|
||||
```bash
|
||||
go run ./scripts/sync_eino_ext_skills.go -src /path/to/eino-ext -dest ./skills/eino-ext -clean
|
||||
EINO_EXT_SKILLS_DIR="$(pwd)/skills/eino-ext" go run .
|
||||
```
|
||||
|
||||
## 从文本到 UI:为什么需要 A2UI
|
||||
|
||||
前八章我们实现的 Agent 只输出文本,但现代 AI 应用需要更丰富的交互。
|
||||
|
||||
**纯文本输出的局限:**
|
||||
|
||||
- 无法展示结构化数据(表格、列表、卡片等)
|
||||
- 无法实时更新(进度条、状态变化等)
|
||||
- 无法嵌入交互元素(按钮、表单、链接等)
|
||||
- 无法支持多媒体(图片、视频、音频等)
|
||||
|
||||
**A2UI 的定位:**
|
||||
|
||||
- **A2UI 是 Agent 到 UI 的协议**:定义了 Agent 输出如何映射到 UI 组件
|
||||
- **A2UI 支持流式渲染**:组件可以实时更新,无需等待完整响应
|
||||
- **A2UI 是声明式的**:Agent 只需声明"显示什么",UI 负责渲染
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **纯文本输出** = "终端命令行"(只能显示文本)
|
||||
- **A2UI** = "Web 应用"(可以显示任何 UI 组件)
|
||||
|
||||
## 关键概念
|
||||
|
||||
### A2UI v0.8 子集(本示例的边界)
|
||||
|
||||
本 quickstart 并没有实现一个“完整的 A2UI 标准库”,而是实现了一个 **A2UI v0.8 的子集**:目标是把 Agent 的事件流,以稳定、可增量渲染的 UI 组件树方式推给浏览器。
|
||||
|
||||
当前实现的 A2UI 消息类型与组件类型,以 [a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go) 为准。
|
||||
|
||||
### A2UI 消息:BeginRendering / SurfaceUpdate / DataModelUpdate / InterruptRequest
|
||||
|
||||
每一行 SSE(`data: {...}`)承载一个 A2UI Message,Message 是一个“信封结构”,每次只会出现一个字段:
|
||||
|
||||
**关键代码片段(注意:这是简化后的代码片段,不能直接运行,完整代码请参考 a2ui/types.go):**
|
||||
|
||||
```go
|
||||
type Message struct {
|
||||
BeginRendering *BeginRenderingMsg
|
||||
SurfaceUpdate *SurfaceUpdateMsg
|
||||
DataModelUpdate *DataModelUpdateMsg
|
||||
DeleteSurface *DeleteSurfaceMsg
|
||||
InterruptRequest *InterruptRequestMsg
|
||||
}
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `BeginRendering`:告诉前端“开始渲染一个 surface(会话)”,并指定根节点 ID
|
||||
- `SurfaceUpdate`:新增/更新一批组件(组件是一个树,用 `id` 互相引用)
|
||||
- `DataModelUpdate`:更新 data bindings(用于把流式文本增量更新到某个 Text 组件)
|
||||
- `InterruptRequest`:当 Agent 触发 interrupt(例如审批)时,通知前端展示批准/拒绝入口
|
||||
|
||||
### A2UI 组件:Text / Column / Card / Row
|
||||
|
||||
本示例 UI 组件只实现了 4 种(见 [a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go)):
|
||||
|
||||
- `Text`:文本渲染(支持 `usageHint` 区分 caption/body/title);当 `dataKey` 存在时,文本来自 `DataModelUpdate`
|
||||
- `Column` / `Row`:布局(children 是组件 ID 列表)
|
||||
- `Card`:卡片容器(children 是组件 ID 列表)
|
||||
|
||||
## A2UI 的实现:把 AgentEvent 转成 A2UI SSE
|
||||
|
||||
最终 Web 版的核心链路是:
|
||||
|
||||
- 后端运行 Agent,得到 `*adk.AsyncIterator[*adk.AgentEvent]`
|
||||
- 把事件流转换为 A2UI JSONL/SSE 流输出给浏览器(见 [a2ui/streamer.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/streamer.go))
|
||||
- 前端解析 SSE 的 `data:` 行并渲染组件树(见 [static/index.html](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/static/index.html))
|
||||
|
||||
### 服务端路由(高层)
|
||||
|
||||
与 A2UI 相关的关键接口(见 [server/server.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/server/server.go)):
|
||||
|
||||
- `GET /`:返回前端页面 `static/index.html`
|
||||
- `POST /sessions/:id/chat`:返回 SSE 流(A2UI messages),把 Agent 运行结果边跑边渲染到 UI
|
||||
- `GET /sessions/:id/render`:返回 JSONL(A2UI messages),用于“选中会话时回放历史”
|
||||
- `POST /sessions/:id/approve`:处理 interrupt 的批准/拒绝并继续返回 SSE 流
|
||||
|
||||
### 事件流转换(高层)
|
||||
|
||||
服务端把 `Runner.Run(...)` 的事件流交给 `a2ui.StreamToWriter(...)`,后者负责:
|
||||
|
||||
- 对 user/assistant/tool 的输出做拆分
|
||||
- 把 tool call / tool result 渲染成 “chip 卡片”
|
||||
- 把 assistant 的流式 token 做成 `DataModelUpdate`,实现“边生成边渲染”
|
||||
- 遇到 interrupt 时发送 `InterruptRequest`,并暂停等待人类批准
|
||||
|
||||
## 前端集成:fetch + SSE(不是 WebSocket)
|
||||
|
||||
- 前端通过 `fetch('/sessions/:id/chat')` 发起请求,然后从 `res.body` 读取流式字节,按行切分并解析 `data: {...}` 的 JSON(见 [static/index.html](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/static/index.html))。
|
||||
|
||||
**关键代码片段(注意:这是简化后的代码片段,不能直接运行,完整代码请参考 static/index.html):**
|
||||
|
||||
```javascript
|
||||
const res = await fetch(`/sessions/${id}/chat`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({message}),
|
||||
});
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('data:')) {
|
||||
const jsonStr = trimmed.slice(5).trimStart();
|
||||
processA2UIMessage(JSON.parse(jsonStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## A2UI 流式渲染流程(概览)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 用户:分析这个文件 │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 开始处理 │
|
||||
│ A2UI: AddText │
|
||||
│ "正在分析..." │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 调用 Tool │
|
||||
│ A2UI: AddProgress │
|
||||
│ 进度: 0% │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Tool 执行中 │
|
||||
│ A2UI: UpdateProgress│
|
||||
│ 进度: 50% │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Tool 完成 │
|
||||
│ A2UI: tool result │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 显示结果 │
|
||||
│ A2UI: DataModelUpdate│
|
||||
│ (流式更新 assistant)│
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **A2UI**:Agent 到 UI 的协议,定义了 Agent 输出如何映射到 UI 组件
|
||||
- **子集实现**:本示例只实现了 Text/Column/Card/Row 与 data binding
|
||||
- **流式输出**:后端以 SSE 推送 A2UI JSONL,前端增量渲染组件树
|
||||
- **事件到 UI**:把 `AgentEvent` 转为 `tool call / tool result / assistant stream` 的可视化输出
|
||||
|
||||
## 系列收尾:这个 Quickstart Agent 的完整愿景
|
||||
|
||||
到本章为止,我们用一个可以实际运行的 Agent 串起了 Eino 的核心能力。你可以把它理解为一个可扩展的“端到端 Agent 应用骨架”:
|
||||
|
||||
- 运行时:Runner 驱动执行,支持流式输出与事件模型
|
||||
- 工具层:Filesystem / Shell 等 Tool 能力接入,工具错误可被安全处理
|
||||
- 中间件:可插拔的 middleware/handler,用于错误处理、重试、审批等横切能力
|
||||
- 可观测:callbacks/trace 能力把关键链路打通,便于调试与线上观测
|
||||
- 人机协作:interrupt/resume + checkpoint 支持审批、补参、分支选择等交互式流程
|
||||
- 确定性编排:compose(graph/chain/workflow)把复杂业务流程组织为可维护、可复用的执行图
|
||||
- 业务交付:像 A2UI 这样的 UI 集成,属于业务层自由选择的一环,用来把 Agent 能力以合适的产品形态呈现给用户
|
||||
|
||||
你可以在这个骨架上逐步替换/扩展任意环节:模型、工具、存储、工作流、前端渲染协议,而不需要推倒重来。
|
||||
|
||||
## 扩展思考
|
||||
|
||||
**其他组件类型:**
|
||||
|
||||
- 图表组件(折线图、柱状图、饼图)
|
||||
- 地图组件
|
||||
- 时间线组件
|
||||
- 树形组件
|
||||
- 标签页组件
|
||||
|
||||
**高级功能:**
|
||||
|
||||
- 组件交互(点击、拖拽、输入)
|
||||
- 条件渲染
|
||||
- 组件动画
|
||||
- 响应式布局
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-24"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第九章:Skill(Console)
|
||||
weight: 9
|
||||
---
|
||||
|
||||
本章目标:在第八章(RAG + Interrupt/Resume + Checkpoint)基础上,引入 `skill` 中间件,让 Agent 可以发现并加载一组可复用的技能文档(`SKILL.md`),并在需要时通过工具调用使用它们。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch09/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch09/main.go)
|
||||
- 同步脚本:[scripts/sync_eino_ext_skills.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/scripts/sync_eino_ext_skills.go)
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)
|
||||
- 准备好 `eino-ext` PR 提供的 skills(`eino-guide` / `eino-component` / `eino-compose` / `eino-agent`)
|
||||
|
||||
为什么是这四个?
|
||||
|
||||
ChatWithEino 的定位是“帮用户学习 Eino 框架、并尝试用 AI 辅助写 Eino 代码”。这四个 skills 正好覆盖了这个目标所需的关键知识面:
|
||||
|
||||
- `eino-guide`:学习入口与导航(从哪里开始、怎么快速跑起来)
|
||||
- `eino-component`:Component 接口与各类实现参考(Model/Embedding/Retriever/Tool/Callback 等)
|
||||
- `eino-compose`:编排与确定性工作流参考(Graph/Chain/Workflow 等)
|
||||
- `eino-agent`:ADK/Agent 相关参考(Agent、Runner、Middleware、Filesystem、Human-in-the-loop 等)
|
||||
|
||||
skills 的来源可以是:
|
||||
|
||||
- `eino-ext` 仓库本地路径(脚本会自动读取 `<src>/skills/...`)
|
||||
- 或你已安装 skills 的目录(目录下能看到上述四个子目录)
|
||||
|
||||
## 从 Graph Tool 到 Skill:为什么需要“技能文档”
|
||||
|
||||
第八章解决的是“复杂工作流如何做成一个可调用的 Tool”(Graph Tool)。但你在构建一个面向框架学习/开发辅助的 Agent 时,还会遇到另一类问题:**如何把一组稳定、可复用的知识与指令注入到 Agent 里,并让它在运行时按需加载?**
|
||||
|
||||
这就是 Skill 的定位:
|
||||
|
||||
- **Tool** 更像“动作/能力”:读文件、跑 workflow、调用外部系统
|
||||
- **Skill** 更像“可复用的知识/指令包”:用一组 markdown(`SKILL.md` + `reference/*.md`)描述“如何做某类事”
|
||||
|
||||
简单类比:
|
||||
|
||||
- **Tool** = “能做什么”(函数/接口)
|
||||
- **Skill** = “怎么做”(可复用的说明书/操作手册)
|
||||
|
||||
## 运行
|
||||
|
||||
在 `quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
### 1) 同步 eino-ext skills 到本地目录
|
||||
|
||||
为了让 `skill` 中间件可以“发现”这些 skills,需要把它们放到一个统一目录下,并满足扫描约定:
|
||||
|
||||
- `EINO_EXT_SKILLS_DIR/<skillName>/SKILL.md`
|
||||
|
||||
同步命令(推荐):
|
||||
|
||||
```bash
|
||||
go run ./scripts/sync_eino_ext_skills.go -src /path/to/eino-ext -dest ./skills/eino-ext -clean
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `-src` 支持两种形式:
|
||||
- `eino-ext` 仓库根目录(脚本会自动读取 `<src>/skills/...`)
|
||||
- 你已安装 skills 的目录(目录下应包含 `eino-guide/`、`eino-component/` 等子目录)
|
||||
- `-dest` 默认是 `./skills/eino-ext`(可以省略)
|
||||
|
||||
### 2) 启动 Chapter 9
|
||||
|
||||
```bash
|
||||
EINO_EXT_SKILLS_DIR=/absolute/path/to/chatwitheino/skills/eino-ext go run ./cmd/ch09
|
||||
```
|
||||
|
||||
输出示例(节选):
|
||||
|
||||
```
|
||||
Skills dir: /.../skills/eino-ext
|
||||
Enter your message (empty line to exit):
|
||||
```
|
||||
|
||||
## 在 DeepAgent 中启用 Skill
|
||||
|
||||
本章的 “Skill 可被调用” 不是自动发生的,你需要在 Agent 构建时把 `skill` 中间件注册进去。核心就是三步:
|
||||
|
||||
1. 用本地 filesystem backend(本章用 `eino-ext/adk/backend/local`)提供文件读取/Glob 能力
|
||||
2. 用 `skill.NewBackendFromFilesystem` 把 `EINO_EXT_SKILLS_DIR` 变成一个 Skill Backend
|
||||
3. 用 `skill.NewMiddleware` 生成中间件,并把它塞进 DeepAgent 的 `Handlers`
|
||||
|
||||
**关键代码片段(注意:这是简化后的代码片段,不能直接运行,完整代码请参考 ****cmd/ch09/main.go****):**
|
||||
|
||||
```go
|
||||
backend, _ := localbk.NewBackend(ctx, &localbk.Config{})
|
||||
|
||||
skillBackend, _ := skill.NewBackendFromFilesystem(ctx, &skill.BackendFromFilesystemConfig{
|
||||
Backend: backend,
|
||||
BaseDir: skillsDir, // = $EINO_EXT_SKILLS_DIR
|
||||
})
|
||||
skillMiddleware, _ := skill.NewMiddleware(ctx, &skill.Config{
|
||||
Backend: skillBackend,
|
||||
})
|
||||
|
||||
agent, _ := deep.New(ctx, &deep.Config{
|
||||
ChatModel: cm,
|
||||
Backend: backend,
|
||||
StreamingShell: backend,
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
skillMiddleware,
|
||||
// ... 其他中间件,比如 approval/safeTool/retry 等
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
补充说明:
|
||||
|
||||
- 本 quickstart 为了保证 “没配置 skills 也能跑”,在代码里对 `EINO_EXT_SKILLS_DIR` 做了存在性检查:目录存在才注册 `skillMiddleware`;否则跳过(此时仍可对话与使用 RAG 工具)。
|
||||
- Skill 工具的入参是一个 JSON:`{"skill": "<skillName>"}`,例如 `{"skill":"eino-guide"}`。
|
||||
|
||||
## 快速验证(推荐)
|
||||
|
||||
启动后输入一条指令,明确要求模型调用 skill 工具(用于验证 skills 已被发现且可被加载):
|
||||
|
||||
```
|
||||
Use the skill tool with skill="eino-guide" and tell me what the entry point is for getting started.
|
||||
```
|
||||
|
||||
你应当能在控制台看到类似输出:
|
||||
|
||||
- `[tool result] Launching skill: eino-guide`
|
||||
- Tool result 中包含 `Base directory for this skill: .../eino-guide`
|
||||
|
||||
## 你会看到什么
|
||||
|
||||
- 当模型调用 skill 工具时,控制台会打印:
|
||||
- `[tool call] ...`
|
||||
- `[tool result] ...`(对结果做了截断展示)
|
||||
- 会话保存在 `SESSION_DIR`(默认 `./data/sessions`),支持恢复:
|
||||
- `go run ./cmd/ch09 --session <id>`
|
||||
Reference in New Issue
Block a user