跳到主要内容

创建日期: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万人。

注意读输出里的三件事:

  1. 模型一次并行提议了两个工具(时间 + 人口,互不依赖一起调)——执行器按顺序执行并回填;
  2. [思考] 是模型的 thinking 块(推理模型先想后答),不是最终文本;真正给用户的答案在「回答:」后面;
  3. 「回答:」出现 = 这一轮 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、写个总结文件」时,它会分好几轮、轮里可能有并行工具——下一篇让这个流程真的落盘

动手

  1. 给 tools 加一个 city_weather(city)(返回一段固定文案),问「上海天气怎么样」看它会不会调;
  2. maxTokens 调小(如 64)问复杂问题,观察输出被截断;
  3. 故意让它调用一个不存在的工具名,看 toolUses 找不到时的兜底。

自测

  1. 循环什么条件下退出?
  2. 三个工具并行提议时,回填应该怎么组织消息?
  3. thinkingtext 哪个是最终答案?取正文用什么函数?
  4. 最大轮数起了什么作用?
  5. 工具白名单意味着模型能干什么、不能干什么?

下一篇:复杂 Agent:多工具 + 多步编排 + 落盘为准