16 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-04-29 15:30 |
第四章:Tool 与文件系统访问
概述
本章为 Agent 引入 Tool(工具)能力,使其能够突破纯文本对话的边界,直接操作文件系统、搜索代码库、执行命令。通过 DeepAgent 预构建组件和 Backend 抽象接口,只需几行配置即可让 Agent「看见」并「触碰」真实世界。
为什么需要 Tool
前三章我们实现的 Agent 只能对话,无法执行实际操作。
Agent 的局限:
- 只能生成文本回复
- 无法访问外部资源(文件、API、数据库等)
- 无法执行实际任务(计算、查询、修改等)
Tool 的定位:
- Tool 是 Agent 的能力扩展:让 Agent 能够执行具体操作
- Tool 封装了具体实现:Agent 不关心 Tool 内部如何工作,只关心输入输出
- Tool 可组合:一个 Agent 可以有多个 Tool,根据需要选择调用
简单类比:
- Agent = "智能助手"(能理解指令,但需要工具才能执行)
- Tool = "工具箱"(文件操作、网络请求、数据库查询等)
[!question] 深入思考
如果 Agent 拥有无限个 Tool,会不会反而变得更差?
提示:考虑模型上下文窗口限制、Token 成本、以及"选择困难症"效应。实际设计中,工具的元信息描述质量比数量更重要——一个好的Description能让模型精准选对工具。
为什么需要文件系统能力
本示例是 ChatWithDoc(与文档对话),目标是帮助用户学习 Eino 框架并编写 Eino 代码。那么,最好的文档是什么?
答案就是:Eino 仓库的代码本身。
| 资源类型 | 价值 |
|---|---|
| Code | 源代码展示了框架的真实实现 |
| Comment | 代码注释提供了设计思路和使用说明 |
| Examples | 示例代码演示了最佳实践 |
通过文件系统访问能力,Agent 可以直接读取 Eino 源码、注释和示例,为用户提供最准确、最及时的技术支持。
[!tip] 现实启发
很多官方文档会过时或写得模糊,但代码不会。让 Agent "读源码"是一种绕过信息衰减的可靠策略——这也是为什么 RAG 系统的向量库经常直接索引代码仓库的原因。
关键概念
Tool 接口
Tool 是 Eino 中定义可执行能力的接口:
// 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:信息层,只提供工具的名称、描述和参数 schema,ChatModel 据此决定是否调用InvokableTool:同步执行层,返回完整结果字符串,适合大多数文件操作场景StreamableTool:流式执行层,通过StreamReader逐步输出结果,适合大量输出的场景(如长文件读取或命令执行)
[!note] 设计要点
为什么 Tool 的参数是 JSON 字符串而不是 Go struct?
因为 Tool 需要在 ChatModel 的上下文里传递——模型只能理解文本。所以 Eino 将参数序列化为 JSON 传给模型,模型再返回 JSON,由 ToolsNode 反序列化后调用底层实现。这种设计让任何语言/协议的工具都能与基于 JSON 的大模型对齐。
Backend 接口
Backend 是 Eino 中用于文件系统操作的抽象接口,定义了六种核心文件能力:
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
}
[!tip] 方法分组
Backend 的六类方法可以归为三类: 发现(LsInfo / GlobInfo)、检索(Read / GrepRaw)、修改(Write / Edit)。记住这个分类有助于快速理解不同 Backend 实现的侧重点。
LocalBackend
LocalBackend 是 Backend 的本地文件系统实现,直接访问操作系统的文件系统:
import localbk "github.com/cloudwego/eino-ext/adk/backend/local"
backend, err := localbk.NewBackend(ctx, &localbk.Config{})
特点:
- 直接访问本地文件系统,使用 Go 标准库实现
- 支持所有 Backend 接口方法
- 支持执行 shell 命令(ExecuteStreaming)
- 路径安全:要求使用绝对路径,防止目录遍历攻击
- 零配置:开箱即用,无需额外设置
[!warning] 安全问题
LocalBackend 要求使用绝对路径来防止
../目录遍历攻击。这意味着 Agent 永远不能通过构造恶意文件名跳出设定的根目录范围——这是 LLM 集成中的关键安全措施。
实现:使用 DeepAgent
本章使用 DeepAgent 预构建 Agent,它提供了 Backend 和 StreamingShell 的一级配置,可以方便地注册文件系统相关的工具。
从 ChatModelAgent 到 DeepAgent:何时需要切换?
前面章节一直使用 ChatModelAgent,它已经能处理多轮对话。但要访问文件系统,我们需要切换到 DeepAgent。
ChatModelAgent vs DeepAgent 对比:
| 能力 | ChatModelAgent | DeepAgent |
|---|---|---|
| 多轮对话 | ✅ | ✅ |
| 添加自定义 Tool | ✅ 手动注册每个 Tool | ✅ 手动注册或自动注册 |
| 文件系统访问(Backend) | ❌ 需手动创建并注册所有文件工具 | ✅ 一级配置,自动注册 |
| 命令执行(StreamingShell) | ❌ 需手动创建 | ✅ 一级配置,自动注册 |
| 内置任务管理 | ❌ | ✅ `write_todos` 工具 |
| 支持子 Agent | ❌ | ✅ |
[!tip] 选择建议
- 纯对话场景(无外部访问)→ 用
ChatModelAgent- 需要访问文件系统或执行命令 → 用
DeepAgent
为什么使用 DeepAgent?
相比直接使用 ChatModelAgent,DeepAgent 的优势在于把「基础设施」封装成了第一类配置,你只需声明意图而非实现细节:
- 一级配置:Backend 和 StreamingShell 直接在 Config 中传入,无需自己组装 ToolsNode
- 自动注册工具:配置 Backend 后自动注册文件系统工具,免去逐个定义的样板代码
- 内置任务管理:提供
write_todos工具,支持复杂任务的规划与跟踪 - 支持子 Agent:可以配置专门的子 Agent 处理特定任务
- 更强大:集成了文件系统、命令执行等多种能力
代码实现
这段代码完成了两件事:创建 Backend 实例,再将其注入 DeepAgent。让我们逐行看:
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{})
// 第二步:将 backend 注入 DeepAgent,它会自动注册文件相关 Tool
agent, err := deep.New(ctx, &deep.Config{
Name: "Ch04ToolAgent", // Agent 的名称
Description: "ChatWithDoc agent with filesystem access.",
ChatModel: cm, // 底层大模型
Instruction: instruction, // 系统提示词
Backend: backend, // 文件系统操作能力
StreamingShell: backend, // 命令执行能力
MaxIteration: 50, // 最大思考-行动循环次数
})
[!note] MaxIteration 的含义
每次 Agent 「思考 → Tool Call → 观察结果」算一次迭代。设为 50 意味着最多允许 50 轮。如果达到上限仍未得到满意结果,会返回部分完成的内容。设置过高会浪费 Token,过低则可能让 Agent 中途放弃。后续章节会讨论如何优化这个值。
DeepAgent 自动注册的工具
当配置了 Backend 和 StreamingShell 后,DeepAgent 会自动注册以下工具:
| 工具名 | 对应 Backend 方法 | 用途 |
|---|---|---|
read_file |
Read |
读取文件内容 |
write_file |
Write |
写入文件内容 |
edit_file |
Edit |
编辑文件内容 |
glob |
GlobInfo |
根据 glob 模式查找文件 |
grep |
GrepRaw |
在文件中搜索内容 |
execute |
shell 命令 | 执行 shell 命令 |
[!note] 从接口到 Tool 的映射
Backend 定义了 6 个方法(LsInfo / Read / GrepRaw / GlobInfo / Write / Edit),但 DeepAgent 只注册了 5 个 Tool(少了 LsInfo)。这是因为
glob已经能完成目录探索的需求。如果未来需要更详细的列表信息,可以手动补充 LsInfo 对应的 Tool。
代码位置
- 入口代码:cmd/ch04/main.go
前置条件
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。
本章还需要设置 PROJECT_ROOT(可选,见下方运行说明)。
运行
在 examples/quickstart/chatwitheino 目录下执行:
# 可选:设置 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 的完整使用场景。
推荐的三仓库目录结构(如要完整体验):
graph LR
ER[eino 核心库\nPROJECT_ROOT] --> ADK[adk/]
ER --> COMP[components/]
ER --> COMPOSE[compose/]
ER --> EXT[eino-ext 扩展库]
ER --> EX[eino-examples 示例库]
EX --> QS[quickstart/]
QS --> CW[chatwitheino / 本示例]
style ER fill:#e3f2fd
style CW fill:#fff3e0
[!example] PROJECT_ROOT 的两种用法
场景一:快速验证(不设置)
Agent 只能访问chatwitheino目录下的文件,适合学习当前示例的代码结构。场景二:完整对话(设置
export PROJECT_ROOT=/path/to/eino)
Agent 可以搜索 Eino 框架全部源码——比如查找某个 API 的实现细节、阅读组件设计思路等。这是 ChatWithEino 真正「与文档对话」的价值所在。
可以使用 dev_setup.sh 脚本自动设置上述目录结构:
# 在 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 时,内部会经历以下循环。你可以把整个过程理解为「思考 → 行动 → 观察」的闭环:
flowchart LR
U[用户提问] --> A{Agent\n分析意图}
A -->|纯对话| R[直接回复]
A -->|需要工具| T1[生成 Tool Call\n参数 JSON]
T1 --> T2[执行 Tool\n读取/写入/搜索等]
T2 --> T3[返回 Tool Result]
T3 --> A2{Agent\n整合信息}
A2 --> R2[生成最终回复]
R2 --> U2[回复用户]
style A fill:#e1f5fe
style T2 fill:#fff3e0
style T3 fill:#e8f5e9
以"列出当前目录的文件"为例:
[!example] 逐步拆解
Step 1 — 意图识别
用户说"列出文件",Agent 判断这不是纯对话请求,而是文件系统操作意图。Step 2 — 工具选择与参数生成
Agent 从可用工具中选择glob(查找文件列表),生成参数{"pattern": "*"}。Step 3 — Tool 执行
ToolsNode接收 JSON 参数,反序列化后调用GlobInfo,将结果序列化为 JSON 返回。Step 4 — 结果整合
Agent 收到["main.go", "go.mod", ...],整理成自然语言回复:"找到 5 个文件..."。
你是否有想过——Agent 是如何知道该选哪个工具的?关键在于 BaseTool.Info() 返回的 元信息(名称、描述、参数 schema)。ChatModel 基于这些信息来决定调用哪个 Tool 以及传入什么参数。这就是后面会详细展开的 Function Calling 机制。
本章小结
| 概念 | 一句话理解 |
|---|---|
| Tool | Agent 的能力扩展,让它能执行具体操作而非仅对话 |
| Backend | 文件系统操作的抽象接口,定义发现、检索、修改三类方法 |
| LocalBackend | Backend 的本地实现,开箱即用且路径安全 |
| DeepAgent | 预构建的高级 Agent,配置 Backend 即可自动获得文件能力 |
| 自动注册 | 声明式配置优于命令式组装——传一个 Backend 就得到五个工具 |
| 调用流程 | 意图分析 → Tool Call → 执行 → 结果 → 回复(闭环迭代) |
扩展思考
其他 Tool 类型
当前我们只用了文件系统相关 Tool。Eino 生态中还支持更多类型:
- HTTP Tool:调用外部 API,让 Agent 具备联网能力
- Database Tool:查询数据库,适用于数据分析场景
- Calculator Tool:精确计算,弥补大模型不擅长数学的问题
- Code Executor Tool:运行代码,适合生成并验证算法实现
自定义 Tool 创建
除了使用 DeepAgent 自动注册的 Tool,你还可以手动创建。最简单的方式是使用 utils.InferTool 从函数签名自动推断参数 schema:
// 写一个普通 Go 函数...
func Greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
// ...一行代码转成 Tool
greetTool := utils.InferTool(Greet)
如果你想了解更多,详见:
关联笔记
- Eino/quick_start/_index
- Eino/quick_start/chapter_03_memory_and_session — Agent 的记忆与会话管理(上一章)
- Eino/quick_start/chapter_05_middleware — Tool 调用中间件与拦截器(下一章)