---
tags: ["Eino", "Agent", "Tool", "Backend", "DeepAgent", "文件系统"]
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 中定义可执行能力的接口：

```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`：**信息层**，只提供工具的名称、描述和参数 schema，ChatModel 据此决定是否调用
- `InvokableTool`：**同步执行层**，返回完整结果字符串，适合大多数文件操作场景
- `StreamableTool`：**流式执行层**，通过 `StreamReader` 逐步输出结果，适合大量输出的场景（如长文件读取或命令执行）

> [!note] 设计要点
>
> 为什么 Tool 的参数是 JSON 字符串而不是 Go struct？  
> 因为 Tool 需要在 ChatModel 的上下文里传递——模型只能理解文本。所以 Eino 将参数序列化为 JSON 传给模型，模型再返回 JSON，由 ToolsNode 反序列化后调用底层实现。这种设计让任何语言/协议的工具都能与基于 JSON 的大模型对齐。

### 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
}
```

> [!tip] 方法分组
>
> Backend 的六类方法可以归为三类：
> **发现**（LsInfo / GlobInfo）、**检索**（Read / GrepRaw）、**修改**（Write / Edit）。记住这个分类有助于快速理解不同 Backend 实现的侧重点。

### 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）
- 路径安全：要求使用绝对路径，防止目录遍历攻击
- 零配置：开箱即用，无需额外设置

> [!warning] 安全问题
>
> LocalBackend 要求使用绝对路径来防止 `../` 目录遍历攻击。这意味着 Agent 永远不能通过构造恶意文件名跳出设定的根目录范围——这是 LLM 集成中的关键安全措施。

## 实现:使用 DeepAgent

本章使用 DeepAgent 预构建 Agent,它提供了 Backend 和 StreamingShell 的一级配置,可以方便地注册文件系统相关的工具。

### 从 ChatModelAgent 到 DeepAgent：何时需要切换？

前面章节一直使用 `ChatModelAgent`,它已经能处理多轮对话。但要访问文件系统，我们需要切换到 `DeepAgent`。

**ChatModelAgent vs DeepAgent 对比：**

<table>
<tr><th>能力</th><th>ChatModelAgent</th><th>DeepAgent</th></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>✅ `write_todos` 工具</td></tr>
<tr><td>支持子 Agent</td><td>❌</td><td>✅</td></tr>
</table>

> [!tip] 选择建议
>
> - 纯对话场景（无外部访问）→ 用 `ChatModelAgent`
> - 需要访问文件系统或执行命令 → 用 `DeepAgent`

### 为什么使用 DeepAgent?

相比直接使用 ChatModelAgent，DeepAgent 的优势在于把「基础设施」封装成了第一类配置，你只需声明意图而非实现细节：

1. **一级配置**：Backend 和 StreamingShell 直接在 Config 中传入，无需自己组装 ToolsNode
2. **自动注册工具**：配置 Backend 后自动注册文件系统工具，免去逐个定义的样板代码
3. **内置任务管理**：提供 `write_todos` 工具，支持复杂任务的规划与跟踪
4. **支持子 Agent**：可以配置专门的子 Agent 处理特定任务
5. **更强大**：集成了文件系统、命令执行等多种能力

### 代码实现

这段代码完成了两件事：创建 Backend 实例，再将其注入 DeepAgent。让我们逐行看：

```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{})

// 第二步：将 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](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 的完整使用场景。

**推荐的三仓库目录结构（如要完整体验）：**

```mermaid
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` 脚本自动设置上述目录结构：

```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 时，内部会经历以下循环。你可以把整个过程理解为「思考 → 行动 → 观察」的闭环：

```mermaid
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
// 写一个普通 Go 函数...
func Greet(name string) string {
	return fmt.Sprintf("Hello, %s!", name)
}

// ...一行代码转成 Tool
greetTool := utils.InferTool(Greet)
```

如果你想了解更多，详见：

- [Tool 接口文档](https://github.com/cloudwego/eino/tree/main/components/tool)
- [Tool 创建示例](https://github.com/cloudwego/eino-examples/tree/main/components/tool)

## 关联笔记

- [[Eino/quick_start/_index]]
- [[Eino/quick_start/chapter_03_memory_and_session]] — Agent 的记忆与会话管理（上一章）
- [[Eino/quick_start/chapter_05_middleware]] — Tool 调用中间件与拦截器（下一章）
