LOADING
4713 words
24 minutes
Agent 多工具调用(五)

RAG 让 LLM 能查资料,LangGraph 让 LLM 会思考,但它们都还是「动口不动手」——只能回答问题,不能真正做事。Agent 的本质是让 LLM 拥有「行动能力」:搜索网络、调用 API、读写数据库、执行代码……

什么是 Agent?

一句话定义

Agent = LLM(大脑) + 工具(双手) + 循环(反思)

组件作用
LLM思考:我现在需要做什么?用哪个工具?
工具集行动:搜索、计算、调用 API、读写文件……
循环反思:刚才的结果够了吗?还需要继续吗?

Agent vs 普通 ChatBot

维度普通 ChatBotAgent
能力边界只能用训练数据里的知识可以调用外部工具,能力无限扩展
执行方式单轮调用,直接返回答案多轮循环:思考 → 行动 → 观察 → 再思考
状态无状态(或简单历史)有完整的内部状态:任务、计划、已完成步骤、工具调用记录
输出直接给答案先做事,再给答案(答案是做事的结果)

一个真实的例子

用户问:「2024 年北京的平均房价是多少?」

普通 ChatBotAgent
「对不起,我的训练数据截止到 2023 年,无法回答。」🧠 思考:这个问题需要最新数据,我需要搜索
🔧 调用「网络搜索」工具,搜索「2024 北京平均房价」
📋 拿到结果:「2024 年北京平均房价 6.8 万/㎡」
🧠 思考:信息够了,可以回答了
💬 回答:「根据最新数据,2024 年北京平均房价约为 6.8 万元/平方米」

一、我的 Agent 演进路径

阶段 1:工具模拟(当前 deep-langgraph.ts)

deep-langgraph.ts 中,我已经实现了一个最简单的工具模拟系统:

// src/server/handlers/deep-langgraph.ts:26
interface Tool {
name: string;
description: string;
execute: (input: string) => Promise<string>;
}
const tools: Tool[] = [
{
name: "search",
description: "搜索网络信息",
execute: async (query: string) => {
// 模拟搜索,实际可以换成真实的搜索引擎 API
const results: Record<string, string> = {
北京美食: "北京著名美食:烤鸭、炸酱面、豆汁焦圈、涮羊肉",
烤鸭历史: "北京烤鸭起源于南北朝,明朝成为宫廷美食",
JavaScript异步: "JavaScript 异步编程主要靠 Promise、async/await",
};
const matched = Object.entries(results).find(([key]) => {
const chars = key.split("");
return chars.every((c) => query.includes(c));
});
return matched ? matched[1] : `未找到关于"${query}"的信息`;
},
},
];

这是一个完整的工具接口定义

  • name: 工具名,LLM 通过名字选择工具
  • description: 工具描述,告诉 LLM 这个工具是做什么的
  • execute: 工具的实际执行逻辑

当前的问题

  • ❌ LLM 知道有工具,但不会主动选择使用
  • ❌ Planner 只是把任务拆成步骤,Executor 还是让 LLM 自己「执行」
  • ❌ 没有真正的「工具调用」环节

阶段 2:真正的 Tool Calling Agent

我们需要在 LangGraph 中加一个「工具选择」节点:

新的图结构

[START]
planner ← 制定计划
tool_selector ← 选择工具 + 生成参数
tool_executor ← 执行工具调用
replanner ← 评估:够了吗?还要继续吗?
↓ ↘
↓ (完成) → [END]
(继续) → tool_selector ← 循环

下面是完整的实现代码:

二、完整的 Tool Calling Agent 实现

第一步:扩展工具系统

先加几个实用的工具:

src/server/agent/tools.ts
export interface Tool {
name: string;
description: string;
// 参数定义(告诉 LLM 这个工具需要什么参数)
parameters: {
type: "object";
properties: Record<string, { type: string; description: string }>;
required: string[];
};
execute: (input: Record<string, any>) => Promise<string>;
}
// 工具 1:网络搜索
export const searchTool: Tool = {
name: "web_search",
description: "搜索网络上的最新信息,适合查询新闻、价格、数据等时效性问题",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "搜索关键词" },
},
required: ["query"],
},
execute: async ({ query }) => {
// 实际项目中换成 SerpAPI / Bing Search / 搜索引擎 API
const mockResults: Record<string, string> = {
"北京房价": "2024 年北京平均房价约 6.8 万元/平方米,海淀约 9.5 万,朝阳约 7.8 万",
"上海房价": "2024 年上海平均房价约 7.2 万元/平方米,浦东约 8.5 万",
"Vue3 教程": "Vue3 官方文档:https://cn.vuejs.org,推荐组合式 API + TypeScript",
};
return mockResults[query] || `搜索"${query}"未找到结果,建议换个关键词试试`;
},
};
// 工具 2:计算器
export const calculatorTool: Tool = {
name: "calculator",
description: "执行数学计算,支持加减乘除、百分比、开方等运算",
parameters: {
type: "object",
properties: {
expression: { type: "string", description: "数学表达式,例如:100 * 0.85" },
},
required: ["expression"],
},
execute: async ({ expression }) => {
try {
// 安全的计算方式
const result = Function('"use strict";return (' + expression + ")")();
return `计算结果:${expression} = ${result}`;
} catch (e) {
return `计算失败:${(e as Error).message}`;
}
},
};
// 工具 3:代码解释器
export const codeInterpreterTool: Tool = {
name: "code_interpreter",
description: "执行 JavaScript 代码,适合数据处理、复杂计算、生成图表等",
parameters: {
type: "object",
properties: {
code: { type: "string", description: "要执行的 JavaScript 代码" },
},
required: ["code"],
},
execute: async ({ code }) => {
try {
// 实际项目中应该用安全的沙箱环境执行
const result = eval(code);
return `代码执行成功\n输出:${JSON.stringify(result)}`;
} catch (e) {
return `代码执行失败:${(e as Error).message}`;
}
},
};
// 工具集合
export const allTools: Tool[] = [
searchTool,
calculatorTool,
codeInterpreterTool,
];

关键设计:每个工具都有 parameters 定义,告诉 LLM 这个工具需要什么参数,以及每个参数的含义。这是 Tool Calling 的核心。

第二步:Agent State 扩展

Agent 需要记录更多状态:工具调用历史、工具返回结果、思考过程等。

src/server/agent/agent-graph.ts
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
import { allTools } from "./tools";
const AgentState = Annotation.Root({
// 原始任务
task: Annotation<string>(),
// 执行计划
plan: Annotation<string[]>({
reducer: (_prev, next) => next,
default: () => [],
}),
// 当前步骤索引
currentStepIndex: Annotation<number>({
reducer: (_prev, next) => next,
default: () => 0,
}),
// 工具调用历史
toolCalls: Annotation<Array<{
toolName: string;
parameters: Record<string, any>;
result: string;
}>>({
reducer: (prev, next) => [...prev, ...next],
default: () => [],
}),
// LLM 的思考过程
thoughts: Annotation<string[]>({
reducer: (prev, next) => [...prev, ...next],
default: () => [],
}),
// 最终结果
result: Annotation<string>({
reducer: (_prev, next) => next,
default: () => "",
}),
});

第三步:核心节点实现

节点 1:Planner — 制定计划

const plannerPrompt = PromptTemplate.fromTemplate(`
你是一个任务规划专家。请分析用户的请求,制定执行计划。
可用工具:
{tools}
请输出 JSON 格式的计划:
{{"steps": ["步骤1的描述", "步骤2的描述", ...]}}
注意:
- 如果需要外部信息,安排搜索步骤
- 如果需要计算,安排计算步骤
- 步骤不超过 5 个
用户请求:{task}
`);
async function planNode(state: typeof AgentState.State, ws: WebSocket) {
sendStatus(ws, "🧠 正在分析任务并制定计划...");
const chain = plannerPrompt.pipe(llm).pipe(parser);
const result = await chain.invoke({
tools: allTools.map(t => `${t.name}: ${t.description}`).join("\n"),
task: state.task,
});
let steps: string[];
try {
const jsonMatch = result.match(/\{[\s\S]*\}/);
steps = jsonMatch ? JSON.parse(jsonMatch[0]).steps : [state.task];
} catch {
steps = [state.task];
}
const planText = steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
sendStatus(ws, `📝 已制定计划:\n${planText}`);
return { plan: steps, currentStepIndex: 0 };
}

节点 2:Tool Selector — 选择工具 + 生成参数

这是 Agent 最核心的节点——决定用哪个工具,以及传什么参数

const toolSelectorPrompt = PromptTemplate.fromTemplate(`
你是一个工具选择专家。请根据当前步骤,选择最合适的工具并生成参数。
当前任务步骤:
{currentStep}
可用工具列表:
{toolsList}
请输出 JSON 格式,选择一个工具:
{{
"thought": "你选择这个工具的原因(1句话)",
"toolName": "工具名称",
"parameters": {{
"参数名1": "参数值1",
"参数名2": "参数值2"
}}
}}
注意:
1. 必须从上面的工具列表中选择,不能自己发明工具
2. 参数必须符合工具的参数定义
3. 如果不需要工具(直接回答即可),toolName 设为 "direct_answer"
`);
async function toolSelectorNode(state: typeof AgentState.State, ws: WebSocket) {
const currentStep = state.plan[state.currentStepIndex];
sendStatus(
ws,
`🔧 正在为步骤 [${state.currentStepIndex + 1}/${state.plan.length}] 选择工具:${currentStep}`,
);
// 把工具列表格式化,告诉 LLM 每个工具的参数要求
const toolsList = allTools.map(tool => {
const paramsDesc = Object.entries(tool.parameters.properties)
.map(([name, prop]) => ` - ${name}: ${prop.description} (${prop.type})`)
.join("\n");
return `${tool.name}: ${tool.description}\n参数:\n${paramsDesc}`;
}).join("\n\n");
const chain = toolSelectorPrompt.pipe(llm).pipe(parser);
const result = await chain.invoke({
currentStep,
toolsList,
});
let decision: {
thought: string;
toolName: string;
parameters: Record<string, any>;
};
try {
const jsonMatch = result.match(/\{[\s\S]*\}/);
decision = jsonMatch ? JSON.parse(jsonMatch[0]) : {
thought: "无法解析,直接回答",
toolName: "direct_answer",
parameters: {},
};
} catch {
decision = {
thought: "解析失败,直接回答",
toolName: "direct_answer",
parameters: {},
};
}
sendStatus(ws, ` 💭 思考:${decision.thought}`);
sendStatus(ws, ` 🎯 选择工具:${decision.toolName}`);
if (Object.keys(decision.parameters).length > 0) {
sendStatus(ws, ` 📋 参数:${JSON.stringify(decision.parameters)}`);
}
return {
thoughts: [decision.thought],
toolCalls: [{
toolName: decision.toolName,
parameters: decision.parameters,
result: "", // 留空,等 executor 执行后填充
}],
};
}

节点 3:Tool Executor — 执行工具调用

async function toolExecutorNode(state: typeof AgentState.State, ws: WebSocket) {
const lastCall = state.toolCalls[state.toolCalls.length - 1];
if (!lastCall || lastCall.result) {
return {};
}
// 直接回答,不需要调用工具
if (lastCall.toolName === "direct_answer") {
sendStatus(ws, " ✅ 不需要调用工具,直接回答");
return {
toolCalls: [{
...lastCall,
result: "直接基于已有信息回答",
}],
};
}
// 找到对应的工具
const tool = allTools.find(t => t.name === lastCall.toolName);
if (!tool) {
const error = `工具不存在:${lastCall.toolName}`;
sendStatus(ws, ` ❌ ${error}`);
return {
toolCalls: [{ ...lastCall, result: error }],
};
}
sendStatus(ws, ` ⚡ 正在执行 ${tool.name}...`);
try {
// 执行工具
const result = await tool.execute(lastCall.parameters);
sendStatus(ws, ` ✅ 工具执行完成`);
sendStatus(ws, ` 📄 结果:${result.slice(0, 100)}${result.length > 100 ? "..." : ""}`);
return {
toolCalls: [{ ...lastCall, result }],
};
} catch (error: any) {
const errorMsg = `工具执行失败:${error.message}`;
sendStatus(ws, ` ❌ ${errorMsg}`);
return {
toolCalls: [{ ...lastCall, result: errorMsg }],
};
}
}

节点 4:Replanner — 评估决策

const replannerPrompt = PromptTemplate.fromTemplate(`
你是一个任务评估专家。请判断当前任务是否已经完成。
原始任务:{task}
已完成的工具调用和结果:
{toolResults}
请输出 JSON 格式的决策:
{{
"status": "complete" 或 "continue",
"reason": "你的判断理由(1句话)",
"result": "如果 complete,这里是最终的总结回答;如果 continue,可以为空"
}}
注意:
- 只有当已有信息足够完整回答用户问题时,才选 complete
- 如果信息不足,选 continue,下一步会继续调用工具
`);
async function replanNode(state: typeof AgentState.State, ws: WebSocket) {
sendStatus(ws, "📋 正在评估执行结果...");
// 格式化工具调用历史
const toolResults = state.toolCalls
.map((call, i) => {
return `调用 ${i + 1}:${call.toolName}\n参数:${JSON.stringify(call.parameters)}\n结果:${call.result}`;
})
.join("\n\n");
const chain = replannerPrompt.pipe(llm).pipe(parser);
const response = await chain.invoke({
task: state.task,
toolResults,
});
let decision: { status: string; reason: string; result?: string };
try {
const jsonMatch = response.match(/\{[\s\S]*\}/);
decision = jsonMatch ? JSON.parse(jsonMatch[0]) : {
status: "complete",
reason: "解析失败,默认完成",
result: "任务已完成",
};
} catch {
decision = {
status: "complete",
reason: "解析失败,默认完成",
result: "任务已完成",
};
}
sendStatus(ws, ` 💭 评估:${decision.reason}`);
if (decision.status === "complete") {
sendStatus(ws, "🎉 任务完成!");
return {
result: decision.result || "任务已完成",
thoughts: [decision.reason],
};
}
// 继续执行下一个步骤
sendStatus(ws, "🔄 继续执行下一步...");
return {
currentStepIndex: state.currentStepIndex + 1,
thoughts: [decision.reason],
};
}

第四步:条件路由

function shouldSelectTool(
state: typeof AgentState.State,
): "tool_selector" | "replanner" | typeof END {
if (state.result) return END;
// 还有步骤没执行完 → 继续选工具
if (state.currentStepIndex < state.plan.length) {
return "tool_selector";
}
// 所有步骤都执行完了 → 去评估
return "replanner";
}
function afterExecute(
state: typeof AgentState.State,
): "replanner" {
// 执行完工具后,去 replanner 评估
return "replanner";
}
function afterReplan(
state: typeof AgentState.State,
): "tool_selector" | typeof END {
if (state.result) return END;
return "tool_selector";
}

第五步:构建完整的 Agent Graph

function createAgentGraph(ws: WebSocket) {
return new StateGraph(AgentState)
.addNode("planner", (s) => planNode(s, ws))
.addNode("tool_selector", (s) => toolSelectorNode(s, ws))
.addNode("tool_executor", (s) => toolExecutorNode(s, ws))
.addNode("replanner", (s) => replanNode(s, ws))
.addEdge(START, "planner")
.addConditionalEdges("planner", shouldSelectTool)
.addEdge("tool_selector", "tool_executor") // 选完工具 → 执行
.addConditionalEdges("tool_executor", afterExecute) // 执行完 → 评估
.addConditionalEdges("replanner", afterReplan) // 评估后 → 继续或结束
.compile();
}

第六步:Handler 入口

export async function handleAgent(
ws: WebSocket,
session: Session,
content: string,
): Promise<void> {
if (!content.trim()) {
ws.send(JSON.stringify({ type: "error", content: "消息不能为空" }));
return;
}
addUserMessage(session, content);
try {
const start = Date.now();
sendStatus(ws, `🤖 启动 Agent 模式...`);
const graph = createAgentGraph(ws);
const result = await graph.invoke({ task: content });
const elapsed = Date.now() - start;
addAIMessage(session, result.result);
ws.send(
JSON.stringify({
type: "done",
mode: "agent",
content: result.result,
elapsed,
summary: {
totalTools: result.toolCalls.length,
toolCalls: result.toolCalls.map(c => ({
tool: c.toolName,
parameters: c.parameters,
result: c.result.slice(0, 200),
})),
},
}),
);
logger.info(
`[${session.id}] 🤖 Agent 完成 (${elapsed}ms, 调用了 ${result.toolCalls.length} 个工具)`,
);
} catch (error: any) {
logger.error(`[${session.id}] Agent 调用失败`, error.message);
ws.send(JSON.stringify({ type: "error", content: `请求失败: ${error.message}` }));
}
}

三、Agent 运行演示

用户提问:「2024 年北京 100 平米的房子总价大概多少?」

Agent 的完整思考和行动过程

🤖 启动 Agent 模式...
🧠 正在分析任务并制定计划...
📝 已制定计划:
1. 搜索 2024 年北京的平均房价
2. 计算 100 平米房子的总价
3. 整理并给出最终答案
🔧 正在为步骤 [1/3] 选择工具:搜索 2024 年北京的平均房价
💭 思考:房价是时效性数据,需要搜索最新信息
🎯 选择工具:web_search
📋 参数:{"query":"北京房价"}
⚡ 正在执行 web_search...
✅ 工具执行完成
📄 结果:2024 年北京平均房价约 6.8 万元/平方米,海淀约 9.5 万,朝阳约 7.8 万
📋 正在评估执行结果...
💭 评估:已获取房价数据,但还需要计算总价
🔄 继续执行下一步...
🔧 正在为步骤 [2/3] 选择工具:计算 100 平米房子的总价
💭 思考:这是数学计算问题,用计算器工具
🎯 选择工具:calculator
📋 参数:{"expression":"6.8 * 100"}
⚡ 正在执行 calculator...
✅ 工具执行完成
📄 结果:计算结果:6.8 * 100 = 680
📋 正在评估执行结果...
💭 评估:已有完整信息,可以回答了
🎉 任务完成!
💬 最终回答:
根据 2024 年的最新数据,北京平均房价约为 6.8 万元/平方米。因此,100 平米的房子总价大约是 680 万元。
注:不同区域差异较大,海淀区约 9.5 万/㎡(100 平米约 950 万),朝阳区约 7.8 万/㎡(100 平米约 780 万)。

四、我的 Agent 工程化经验

经验 1:工具描述是灵魂,写不好 LLM 不会用

错误的工具描述

// ❌ 太模糊,LLM 不知道什么时候用
{ name: "search", description: "搜索信息" }

正确的工具描述

// ✅ 清晰说明适用场景,LLM 知道什么时候该用
{
name: "web_search",
description: "搜索网络上的最新信息,适合查询新闻、价格、数据等时效性问题,或 LLM 知识库中没有的信息",
// ...
}

我的工具描述模板

{工具名}: {一句话说明工具是做什么的}。适合:{适用场景1}、{适用场景2}、{适用场景3}。不适合:{不适用场景}

经验 2:参数定义要精确,不然 LLM 传错参数

错误的参数定义

// ❌ 只有类型,没有说明
parameters: {
query: { type: "string" }
}

正确的参数定义

// ✅ 详细说明每个参数的含义、格式、示例
parameters: {
query: {
type: "string",
description: "搜索关键词,建议用中文,简洁明确,例如:北京房价 2024"
}
}

经验 3:永远要有「直接回答」的兜底

不是所有问题都需要调用工具——简单问题直接回答就行,浪费时间和钱:

// 在 tool_selector 里加入这个兜底
if (不需要工具) {
toolName = "direct_answer"
}
// 在 tool_executor 里处理
if (lastCall.toolName === "direct_answer") {
// 直接返回,不调用任何工具
}

经验 4:工具执行必须有超时和错误处理

外部工具永远是不可靠的:

  • 网络请求可能超时
  • API 可能挂掉
  • 返回格式可能变

我的三层防护

  1. 超时控制:每个工具执行最多等 10 秒
  2. 异常捕获:try-catch 包裹所有 execute 逻辑
  3. 降级策略:工具调用失败时,告诉 LLM「工具不可用,请换一种方式」
// 带超时的工具执行
async function executeWithTimeout(
tool: Tool,
params: Record<string, any>,
timeoutMs = 10000
): Promise<string> {
try {
const result = await Promise.race([
tool.execute(params),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("工具执行超时")), timeoutMs)
),
]);
return result as string;
} catch (error) {
return `工具执行失败:${(error as Error).message},请尝试其他方法或简化问题`;
}
}

经验 5:Agent 必须有「步数限制」,防止死循环

如果不加限制,Agent 可能会无限循环:

  • 搜索 → 结果不对 → 再搜索 → 结果还不对 → 再搜索…

我的解决方案

// 在 State 里加一个步数字段
maxSteps: Annotation<number>({ default: () => 10 });
// 在 replanner 里检查
if (state.toolCalls.length >= state.maxSteps) {
return {
result: "已达到最大执行步数,停止执行。已收集的信息如下:...",
};
}

五、全文总结:整个系列的进化之路

六篇博客的完整脉络

篇序主题核心能力关键技术
第 1 篇四种记忆策略「记住」对话历史Buffer / Window / Summary / Vector
第 2 篇RAG 检索增强「查资料」回答问题文档分割 + 向量化 + 检索链
第 3 篇Prompt Engineering「说人话」输出可控格式角色设定 + 格式约束 + 思维链
第 4 篇工程模块化「可维护」的后端服务分层架构 + 策略模式 + 限流容错
第 5 篇LangGraph 多步推理「会思考」动态规划State + Node + 条件边 + 循环执行
第 6 篇Agent 多工具调用(本文)「能行动」自主做事Tool Calling + 工具选择 + 执行循环

从「工具」到「Agent」的能力跃迁

阶段 0:纯 LLM
只能用训练数据里的知识
阶段 1:有记忆的 ChatBot(第 1 篇)
能记住之前说过什么 → 多轮对话
阶段 2:RAG 问答系统(第 2 篇)
能查资料回答 → 知识扩展到训练数据之外
阶段 3:输出可控(第 3 篇)
能按格式输出 → 机器可解析
阶段 4:可维护的工程(第 4 篇)
能部署到生产环境 → 稳定、可靠
阶段 5:会思考的系统(第 5 篇)
能自己规划步骤 → 从「脚本」到「思考」
阶段 6:能行动的 Agent(第 6 篇)
能调用工具做事 → 从「思考」到「行动」

一句话总结整个系列

我们用六篇博客,把 LLM 从一个「只会聊天的玩具」,一步步变成了一个「能思考、会行动、可维护、能落地的工程系统」。

项目源码:learn-langchain.js

上一篇:[LangGraph 多步推理:从「直线执行」到「有思考的 Agent」](LangGraph 多步推理(四).md)

六、系列完结感言

这六篇博客写的是 learn-langchain.js 这个项目,但其实更像是我自己学习 LLM 工程化的完整笔记。

每一篇都是:

  1. 先写代码——把功能跑起来
  2. 再踩坑——遇到各种奇葩问题
  3. 再重构——把代码从「能跑」变成「好维护」
  4. 再总结——把经验提炼成博客

我相信这就是最好的学习方式:做中学,学中总结

如果你看完这个系列,能从零搭出一个属于自己的 LLM 后端服务——从 RAG 到 Agent,从单文件到分层架构——那这个系列的目标就达成了。

欢迎在 GitHub 上 Star 和提 Issue: 👉 learn-langchain.js

系列完结! 🎉

感谢你的阅读!如果你觉得这个系列对你有帮助,欢迎分享给更多对 LLM 工程化感兴趣的朋友。

下一个系列预告:LLM 前端工程化实战——从零打造一个媲美 ChatGPT 的 AI 对话界面。

Agent 多工具调用(五)
/posts/2026-7-1/5-agent-多工具调用/
Author
Atopos
Published at
2026-07-01
License
CC BY-NC-SA 4.0

Some information may be outdated