创建日期:2026-09-08 | 最近更新:2026-09-08 本机真实运行(DeepSeek Anthropic 兼容端点,
deepseek-v4-flash);输出真实。运行时见上篇的runtime.mjs。
Agent 入门 1:一个「会调用工具」的简单 Agent
上一篇给了公共运行时。这篇把工具调用循环写出来——一个会「查时间 + 查城市人口」的问答 CLI。完整代码跑得动,先跑起来再逐行看懂。
1. 完整代码:simple.mjs
// 简单 Agent:会调用工具的问答循环。
// 模型提议 tool_use → 宿主执行 → 回填 tool_result → 模型继续 → 直到它直接回答。
import { complete, textOf, thinkOf, toolUses, userMsg, modelMsg, config } from './runtime.mjs';
// ---------- 1. 定义工具(纯 JS;模型只“提议”,宿主才执行) ----------
const CITY_DB = {
北京: '2189 万', 上海: '2487 万', 广州: '1881 万', 深圳: '1768 万', 杭州: '1252 万', 成都: '2140 万',
};
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', description: '城市名,如 北京/上海/广州…' } },
required: ['city'],
},
run: (args) => CITY_DB[args.city] ?? `没有「${args.city}」的人口数据(我只有常见城市)`,
},
];
// ---------- 2. 宿主执行工具(真正的“能力”在这里) ----------
function executeTool(tool, args) {
console.log(` ▶ 执行工具 ${tool.name}(${JSON.stringify(args)})`);
return tool.run(args);
}
// ---------- 3. 主循环 ----------
async function run(question) {
console.log(`用户:${question}\n`);
const messages = [userMsg(question)];
for (let turn = 0; turn < 5; turn++) { // 保险丝:最多 5 轮,防死循环
const reply = await complete(messages, { tools, maxTokens: 1024 });
const think = thinkOf(reply); // 推理模型先想一段,打个样
if (think) console.log(`[思考] ${think.split('\n')[0].slice(0, 80)}…`);
messages.push(modelMsg(reply)); // ① 回放 assistant(含 tool_use)
const calls = toolUses(reply);
if (calls.length === 0) { // ② 没有再要工具 → 最终回答
console.log(`回答:${textOf(reply)}\n`);
return;
}
// ③ 协议关键:紧接 assistant 的那条 user 消息里,给它这批 tool_use 一次性全回填
const results = calls.map((c) => {
const tool = tools.find((t) => t.name === c.name);
const result = tool ? executeTool(tool, c.input ?? {}) : `没有 ${c.name} 这个工具`;
return { type: 'tool_result', tool_use_id: c.id, content: String(result) };
});
messages.push({ role: 'user', content: results });
}
console.log('(超过最大轮数,结束)');
}
if (!config.hasKey) { console.error('缺少 ANTHROPIC_AUTH_TOKEN'); process.exit(1); }
run(process.argv.slice(2).join(' ') || '现在几点?顺便帮我用 city_population 查一下北京的人口。');
2. 跑起来
# 三个 env 配好(见上篇 §2)
node simple.mjs "现在几点?请用 get_current_time 获取时间,再用 city_population 查北京人口,然后一句话总结。"
3. 真实运行输出(本机实测)
用户:现在几点?请用 get_current_time 获取时间,再用 city_population 查北京人口,然后一句话总结。
[思考] The user wants me to get the current time and query Beijing's population,
then summarize in one sentence. These are independent calls so I can make them in parallel.…
▶ 执行工具 get_current_time({})
▶ 执行工具 city_population({"city":"北京"})
回答:现在是2026年9月8日下午4点04分,北京的人口约为2189万人。
注意读输出里的三件事:
- 模型一次并行提议了两个工具(时间 + 人口,互不依赖一起调)——执行器按顺序执行并回填;
- [思考] 是模型的
thinking块(推理模型先想后答),不是最终文本;真正给用户的答案在「回答:」后面; - 「回答:」出现 = 这一轮
tool_use为空 → 循环结束。整个闭环:提议→执行→回填→再答。
4. 三句必须记住的实现教训
| # | 教训 | 说明 |
|---|---|---|
| 1 | 一个 assistant 对应一条合并的 tool_result user 消息 | 模型一批提议了 3 个工具,你要在紧接着的一条 user 消息里用 3 个 tool_result 块一起回填,不能一条一个 user(会报 400:tool_use 缺 tool_result) |
| 2 | 过滤 thinking,只取 text | 推理模型 content 里有 thinking 块;最终文本用 textOf(只留 text) |
| 3 | 加最大轮数保险丝 | for (let turn = 0; turn < 5; …)——模型偶尔会绕圈调工具,必须有上限 |
5. 它为什么是「简单」Agent
- 工具只有 2 个、无状态、一次问答一个回合;
- 没有「任务步骤」概念:全靠模型临场决定调用哪些工具;
- 够用来理解循环 + 工具协议,但做不了「分几步完成一个复杂任务」——那是下一篇复杂 Agent 干的事。
6. 进阶预演(下一篇会用到)
- 工具是白名单:这里只给了
get_current_time/city_population,模型想干别的也调不到; - 多步 = 多轮:让模型「查 A、查 B、写个总结文件」时,它会分好几轮、轮里可能有并行工具——下一篇让这个流程真的落盘。
动手
- 给 tools 加一个
city_weather(city)(返回一段固定文案),问「上海天气怎么样」看它会不会调; - 把
maxTokens调小(如 64)问复杂问题,观察输出被截断; - 故意让它调用一个不存在的工具名,看
toolUses找不到时的兜底。
自测
- 循环什么条件下退出?
- 三个工具并行提议时,回填应该怎么组织消息?
thinking和text哪个是最终答案?取正文用什么函数?- 最大轮数起了什么作用?
- 工具白名单意味着模型能干什么、不能干什么?