跳到主要内容

创建日期:2026-09-08 | 最近更新:2026-09-08 本机真实运行(DeepSeek Anthropic 兼容端点,deepseek-v4-flash);输出与产物文件均真实。运行时见篇 0

Agent 入门 2:复杂 Agent——多工具、多步编排、以落盘为准

简单 Agent 只会「一问一答调工具」。真正的工作型 Agent 要把一个任务拆成好几步、每一步都真调工具、最后留下可验证的产物。这篇的 Agent 会:查时间 → 查两个城市人口 → 把结果写成 Markdown 文件 → 列目录确认 → 读回核对 → 给你总结。「完成」以文件真实落盘为准,而不是模型嘴上说写完了。

1. 它比简单 Agent「复杂」在哪

能力简单 Agent复杂 Agent
工具数25(时间/人口/写文件/列目录/读文件)
任务一次问答分 5 步的真实任务(要组织文件)
状态文件系统即记忆(写 report.md 后还能读回)
完成判定模型说不调工具工具结果 + 落盘文件双重确认
健壮性有最大轮数最大轮数 + 单工具 try/catch 兜底 + 收窄到工作区

最核心的一条:它真的在「干活」——模型负责理解和提议,你的工具负责执行并把结果固化到磁盘;下次(甚至别的进程)都能通过 list_files/read_file 看到它做过什么。

2. 代码:complex.mjs

// 复杂 Agent:多工具 + 多步编排 + 文件工作区。
import { mkdirSync, writeFileSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { complete, textOf, thinkOf, toolUses, userMsg, modelMsg, config } from './runtime.mjs';

const WORKSPACE = join(process.cwd(), 'workspace'); // 收窄:只允许写这个目录
mkdirSync(WORKSPACE, { recursive: true });

const CITY_DB = {
北京: '2189 万', 上海: '2487 万', 广州: '1881 万', 深圳: '1768 万',
杭州: '1252 万', 成都: '2140 万', 南京: '949 万', 武汉: '1373 万',
};

// 每个工具 = 输入 Schema + run()。文件工具相对 WORKSPACE 解析 → 天然“沙箱”。
const tools = [
{ name: 'get_current_time', description: '获取当前时间(上海时区)',
input_schema: { type: 'object', properties: {}, required: [] },
run: () => new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) },
{ name: 'city_population', description: '查询某城市人口(万人)',
input_schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
run: (a) => CITY_DB[a.city] ?? `无「${a.city}」数据` },
{ name: 'write_file', description: '写入/覆盖工作区里的一个文件(相对路径)',
input_schema: { type: 'object',
properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
run: (a) => { writeFileSync(join(WORKSPACE, a.path), a.content, 'utf8'); return `已写入 ${a.path}${a.content.length} 字符)`; } },
{ name: 'list_files', description: '列出工作区文件',
input_schema: { type: 'object', properties: {}, required: [] },
run: () => readdirSync(WORKSPACE).join(', ') || '(空)' },
{ name: 'read_file', description: '读取工作区里一个文件的内容',
input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
run: (a) => readFileSync(join(WORKSPACE, a.path), 'utf8') },
];

// 系统提示:把“扮演”和“做”分开。用户任务在下面。
const SYS = '你是用工具完成真实任务的工作型 agent:能查时间/人口、能读写工作区文件。要“做到”,不要“假装做到”。';

const TASK = `你是一个“工作型 agent”,请在本会话里实际使用工具完成:
1) 用 get_current_time 获取当前时间;
2) 用 city_population 分别查询 上海 和 深圳 的人口;
3) 把上面内容整理成 Markdown 报告(含时间 + 两个城市人口的小表格),用 write_file 写入文件 report.md;
4) 用 list_files 确认 report.md 已生成;
5) 用 read_file 读回 report.md,然后给我不超过 2 行的完成总结。
注意:write_file 的 path 直接写 "report.md";不要编造工具结果,每步都真实调用。`;

async function run() {
console.log(`任务:${TASK.split('\n')[0]}…\n`);
const messages = [userMsg(TASK)];
for (let turn = 0; turn < 8; turn++) { // 保险丝
console.log(`— 第 ${turn + 1} 轮 —`);
const reply = await complete(messages, { tools, maxTokens: 2048, system: SYS });

const think = thinkOf(reply);
if (think) console.log(` [思考] ${think.split('\n')[0].slice(0, 90)}`);

messages.push(modelMsg(reply));
const calls = toolUses(reply);
if (calls.length === 0) {
console.log(`完成:${textOf(reply)}\n`);
return;
}
// 同批工具结果一次性回填;单工具 try/catch,失败也回填 is_error 让模型补救
const results = calls.map((c) => {
const tool = tools.find((t) => t.name === c.name);
if (!tool) return { type: 'tool_result', tool_use_id: c.id, content: `没有 ${c.name}` };
try {
const out = tool.run(c.input ?? {});
console.log(`${tool.name}(${JSON.stringify(c.input)}) → ${String(out).slice(0, 80)}`);
return { type: 'tool_result', tool_use_id: c.id, content: String(out) };
} catch (e) {
return { type: 'tool_result', tool_use_id: c.id, content: `工具出错: ${e.message}`, is_error: true };
}
});
messages.push({ role: 'user', content: results });
}
console.log('(达到最大轮数,结束)');
}

if (!config.hasKey) { console.error('缺少 ANTHROPIC_AUTH_TOKEN'); process.exit(1); }
await run();

3. 跑起来

node complex.mjs

4. 真实运行输出(本机实测,4 轮完成)

任务:你是一个“工作型 agent”,请在本会话里实际使用工具完成:…

— 第 1 轮 —
[思考] Let me start by calling the independent tools: get_current_time, and city_population for b…
▶ get_current_time({}) → 2026/9/8 16:04:32
▶ city_population({"city":"上海"}) → 2487 万
▶ city_population({"city":"深圳"}) → 1768 万
— 第 2 轮 —
▶ write_file({"path":"report.md","content":"# 城市人口报告\n…"}) → 已写入 report.md(125 字符)
— 第 3 轮 —
▶ list_files({}) → report.md
▶ read_file({"path":"report.md"}) → # 城市人口报告\n\n**生成时间**:2026/9/8 16:04:32\n… (内容节选)
— 第 4 轮 —
完成:已完成全部5步:查询到当前时间(2026/9/8 16:04:32)、上海2487万、深圳1768万,并写入 report.md,
经 list_files 和 read_file 双重确认文件内容无误。

产物确实落盘(宿主视角,不是模型自述):

$ cat workspace/report.md
# 城市人口报告

**生成时间**:2026/9/8 16:04:32

## 人口查询结果
| 城市 | 人口(万人) |
| --- | --- |
| 上海 | 2487 |
| 深圳 | 1768 |

> 数据来源:实时工具查询结果。

看整条链路:模型负责「决定下一步 + 组织内容」,宿主工具负责「查真实数据、把文件写进磁盘」。第 4 轮它自己用 read_file 把文件读回核对后才收尾——这就是「以工具结果/落盘为准」的自觉。

5. 让它更「复杂/更强」的旋钮(进阶方向)

想加的能力怎么做已有人做好(参考本站)
更多真实工具(网络/浏览器/数据库)加工具;或接 MCP 让第三方提供工具MCP 系列
长期记忆把「记忆」落成文件/向量库,每次任务先读本 agent 的 workspace 就是最朴素的记忆
规划能力让它先输出 plan 再执行,或加一个子 Agent 专职拆解(后续可单独成篇)
不要手搓循环用现成 agent 库(自带的循环更健壮)pi-agent 系列
安全/护栏工具白名单、工作区沙箱、重动作确认、审计参考 InkOS 的确认闸门

为什么这套值得掌握:你从零手写过 tool_result 合并、thinking 过滤、轮数保险丝、工作区沙箱之后,再看任何 agent 框架/MCP/生产 harness,都能一眼看出「它在替你做什么循环、把安全边界放哪」。

6. 三个容易翻车的点

  1. 路径逃逸:文件工具必须把路径 join(WORKSPACE, …) 收窄,否则模型能读/写任意路径(严重安全漏洞);
  2. 假装完成:光看最终文本不保险,要「工具结果 + 落盘文件」双确认——这也是为什么任务里强制它 read_file 核对;
  3. 错误要回填成 is_error:工具抛错别吞掉,回填 is_error: true 的 tool_result,模型会自己改参数/换策略重试。

动手

  1. 把 TASK 改成「查询 3 个城市并生成一个 CSV」;看它会不会用多个 write_file / 自己规划;
  2. 给它一个「读取任意路径」的坏工具(不加 join 收窄),测试路径逃逸,体会沙箱的必要;
  3. 在 workspace 预放一个旧文件,看它 write_file 前会不会先 list_files/read_file(好的 agent 会先侦察)。

自测

  1. 复杂 agent 的「完成」以什么为准?为什么?
  2. 文件工具为什么要相对工作区收窄路径?
  3. 工具抛错时应回填成什么?对模型有什么意义?
  4. 哪一轮体现「模型自己在读回核对」?
  5. 想要「网络搜索」「查数据库」,下一步该加什么(MCP)?

参考