Function Calling 与 AI Agent
问题
什么是 Function Calling?如何构建一个能调用工具、多步推理的 AI Agent?前端如何展示 Agent 的执行过程?
答案
Function Calling 让 LLM 从「只能聊天」进化为能调用外部工具完成实际任务的智能体。它是 AI Agent 的核心技术——LLM 充当「大脑」做决策(调什么工具、传什么参数),你的代码充当「手脚」执行具体操作(API 调用、数据库查询、代码执行等)。
Function Calling ≠ LLM 直接调用函数。LLM 只输出一段 JSON 描述它想调用什么,实际执行完全在你的后端代码中。你对工具的执行拥有完全的控制权(权限验证、超时、日志等)。
一、Agent 核心架构
Agent 循环(Think → Act → Observe)
这就是 ReAct 模式(Reasoning + Acting)的实现:
- Think:LLM 分析当前情况和目标
- Act:LLM 决定调用哪个工具,输出工具名和参数
- Observe:工具执行结果返回给 LLM
- 重复直到 LLM 认为可以回答用户问题
Agent 核心代码
interface ToolDefinition {
name: string;
description: string;
parameters: Record<string, unknown>; // JSON Schema
execute: (args: Record<string, unknown>) => Promise<string>;
}
interface AgentOptions {
maxIterations: number; // 防止死循环
maxToolCalls: number; // 单次最多调几个工具
timeout: number; // 单个工具超时
totalTimeout: number; // 总超时
onStep?: (step: AgentStep) => void; // 实时回调每个步骤
}
interface AgentStep {
type: 'thinking' | 'tool_call' | 'tool_result' | 'answer';
content: string;
toolName?: string;
toolArgs?: Record<string, unknown>;
toolResult?: string;
duration?: number;
status: 'running' | 'done' | 'error';
}
async function agentLoop(
messages: Message[],
tools: ToolDefinition[],
options: AgentOptions
): Promise<{ content: string; steps: AgentStep[]; totalTokens: number }> {
const { maxIterations, maxToolCalls, timeout, totalTimeout, onStep } = options;
const steps: AgentStep[] = [];
let currentMessages = [...messages];
let totalTokens = 0;
const startTime = Date.now();
for (let i = 0; i < maxIterations; i++) {
// 总超时检查
if (Date.now() - startTime > totalTimeout) {
return {
content: '处理超时,已返回当前结果。',
steps,
totalTokens,
};
}
// 1. 调用 LLM
const response = await callLLM({
messages: currentMessages,
tools: tools.map(t => ({
name: t.name,
description: t.description,
parameters: t.parameters,
})),
});
totalTokens += response.usage?.totalTokens ?? 0;
// 2. 无工具调用 → 直接回答,结束循环
if (!response.toolCalls?.length) {
const answerStep: AgentStep = {
type: 'answer',
content: response.content,
status: 'done',
};
steps.push(answerStep);
onStep?.(answerStep);
return { content: response.content, steps, totalTokens };
}
// 3. 有工具调用 → 依次执行
// 先记录 assistant 的工具调用消息
currentMessages.push({
role: 'assistant',
content: response.content || '',
toolCalls: response.toolCalls,
});
// 并行执行所有工具调用(LLM 可以一次返回多个)
const toolPromises = response.toolCalls.slice(0, maxToolCalls).map(async (toolCall) => {
const tool = tools.find(t => t.name === toolCall.name);
// 通知前端:开始执行工具
const callStep: AgentStep = {
type: 'tool_call',
content: '',
toolName: toolCall.name,
toolArgs: toolCall.arguments,
status: 'running',
};
steps.push(callStep);
onStep?.(callStep);
if (!tool) {
const error = `Unknown tool: ${toolCall.name}`;
callStep.status = 'error';
callStep.content = error;
onStep?.(callStep);
return { toolCallId: toolCall.id, content: JSON.stringify({ error }) };
}
try {
// 带超时执行
const startMs = Date.now();
const result = await Promise.race([
tool.execute(toolCall.arguments),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Tool execution timeout')), timeout)
),
]);
callStep.status = 'done';
callStep.duration = Date.now() - startMs;
callStep.toolResult = result;
onStep?.(callStep);
return { toolCallId: toolCall.id, content: result };
} catch (error) {
callStep.status = 'error';
callStep.content = (error as Error).message;
onStep?.(callStep);
return {
toolCallId: toolCall.id,
content: JSON.stringify({ error: (error as Error).message }),
};
}
});
const results = await Promise.allSettled(toolPromises);
// 4. 将工具结果加入消息列表
for (const result of results) {
const value = result.status === 'fulfilled'
? result.value
: { toolCallId: '', content: JSON.stringify({ error: result.reason.message }) };
currentMessages.push({
role: 'tool',
toolCallId: value.toolCallId,
content: value.content,
});
}
// 继续循环,让 LLM 决定下一步
}
return {
content: '达到最大推理步骤,已返回已收集的信息。',
steps,
totalTokens,
};
}
二、工具定义最佳实践
好的工具定义 = LLM 能准确判断「何时调用」+「传什么参数」。
import { z } from 'zod';
// 最佳实践:用 Zod 定义参数 schema,同时获得类型安全和 JSON Schema 输出
// 工具 1:天气查询
const weatherSchema = z.object({
city: z.string().describe('城市名称,如"北京"、"上海"、"Tokyo"'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('温度单位'),
days: z.number().min(1).max(7).default(1).describe('预报天数'),
});
const weatherTool: ToolDefinition = {
name: 'get_weather',
// description 是 LLM 判断是否调用的关键依据,必须清晰
description: '获取指定城市的当前天气和未来天气预报。当用户询问天气、温度、是否需要带伞等问题时调用。',
parameters: zodToJsonSchema(weatherSchema),
execute: async (args) => {
const { city, unit, days } = weatherSchema.parse(args);
const data = await fetch(`/api/weather?city=${city}&unit=${unit}&days=${days}`);
return JSON.stringify(await data.json());
},
};
// 工具 2:数据库查询
const dbQuerySchema = z.object({
query: z.string().describe('SQL SELECT 查询语句(只读)'),
database: z.enum(['users', 'orders', 'products']).describe('要查询的数据库'),
});
const dbQueryTool: ToolDefinition = {
name: 'query_database',
description: '查询数据库获取业务数据。当用户询问用户数、订单量、销售额等需要查询数据库才能回答的问题时调用。只支持 SELECT 查询。',
parameters: zodToJsonSchema(dbQuerySchema),
execute: async (args) => {
const { query, database } = dbQuerySchema.parse(args);
// 安全检查:只允许 SELECT
if (!/^\s*SELECT\b/i.test(query)) {
throw new Error('Only SELECT queries are allowed');
}
const result = await executeReadOnlyQuery(database, query);
return JSON.stringify(result.slice(0, 100)); // 限制结果大小
},
};
// 工具 3:代码执行
const codeRunnerSchema = z.object({
code: z.string().describe('要执行的 JavaScript 代码'),
description: z.string().describe('简要描述代码的作用'),
});
const codeRunnerTool: ToolDefinition = {
name: 'run_code',
description: '在安全沙箱中执行 JavaScript 代码。当需要进行数学计算、数据处理、日期计算或验证代码逻辑时调用。',
parameters: zodToJsonSchema(codeRunnerSchema),
execute: async (args) => {
const { code } = codeRunnerSchema.parse(args);
// 在 VM2 或 WebContainer 沙箱中执行
return await sandboxExecute(code, { timeout: 5000, memoryLimit: '50mb' });
},
};
// 工具 4:网页搜索
const searchSchema = z.object({
query: z.string().describe('搜索关键词'),
type: z.enum(['web', 'news', 'academic']).default('web').describe('搜索类型'),
});
const searchTool: ToolDefinition = {
name: 'web_search',
description: '搜索互联网获取最新信息。当用户的问题涉及最新新闻、实时数据或你不确定的事实时调用。',
parameters: zodToJsonSchema(searchSchema),
execute: async (args) => {
const { query, type } = searchSchema.parse(args);
const results = await searchAPI(query, type);
return JSON.stringify(results.slice(0, 5)); // 只返回前 5 条
},
};
description 直接决定 LLM 是否正确调用工具。常见问题:
- 描述太模糊 → LLM 不确定何时该调用
- 描述缺少使用场景 → LLM 在该调用时不调用
- 多个工具描述有重叠 → LLM 选错工具
好的 description 模板:[功能描述]。当 [使用场景1]、[使用场景2] 时调用。[约束说明]。
三、OpenAI vs Anthropic 工具调用差异
| 差异点 | OpenAI | Anthropic |
|---|---|---|
| 工具定义 | tools[].function.parameters | tools[].input_schema |
| LLM 返回 | message.tool_calls 字段 | content 中的 tool_use block |
| 停止原因 | finish_reason: "tool_calls" | stop_reason: "tool_use" |
| 结果传回 | role: "tool" 消息 | role: "user" 中的 tool_result block |
| 并行调用 | 支持,一次可返回多个 tool_calls | 支持,一次可返回多个 tool_use blocks |
| 强制调用 | tool_choice: { type: "function", function: { name: "xxx" } } | tool_choice: { type: "tool", name: "xxx" } |
| Strict 模式 | strict: true 保证参数 schema | 不支持,依赖 description |
// 统一适配层:将不同 Provider 的工具调用格式统一
interface UnifiedToolCall {
id: string;
name: string;
arguments: Record<string, unknown>;
}
// OpenAI 响应 → 统一格式
function parseOpenAIToolCalls(message: OpenAIMessage): UnifiedToolCall[] {
return (message.tool_calls || []).map(tc => ({
id: tc.id,
name: tc.function.name,
arguments: JSON.parse(tc.function.arguments),
}));
}
// Anthropic 响应 → 统一格式
function parseAnthropicToolCalls(content: ContentBlock[]): UnifiedToolCall[] {
return content
.filter((block): block is ToolUseBlock => block.type === 'tool_use')
.map(block => ({
id: block.id,
name: block.name,
arguments: block.input,
}));
}
// 将工具结果转换为 Provider 特定格式
function buildToolResultMessage(
provider: 'openai' | 'anthropic',
results: Array<{ toolCallId: string; content: string }>
) {
if (provider === 'openai') {
// OpenAI: 每个结果是一条独立的 tool 消息
return results.map(r => ({
role: 'tool' as const,
tool_call_id: r.toolCallId,
content: r.content,
}));
}
// Anthropic: 所有结果放在一条 user 消息中
return [{
role: 'user' as const,
content: results.map(r => ({
type: 'tool_result' as const,
tool_use_id: r.toolCallId,
content: r.content,
})),
}];
}
四、前端 Agent 展示组件
用户需要看到 Agent 的执行过程——每个工具调用的状态、耗时、结果。
import { useState } from 'react';
interface AgentStep {
id: string;
type: 'thinking' | 'tool_call' | 'tool_result' | 'answer';
toolName?: string;
toolArgs?: Record<string, unknown>;
result?: string;
duration?: number;
status: 'running' | 'done' | 'error';
timestamp: number;
}
export function AgentSteps({ steps }: { steps: AgentStep[] }) {
return (
<div className="space-y-2">
{steps.map((step) => (
<AgentStepItem key={step.id} step={step} />
))}
</div>
);
}
function AgentStepItem({ step }: { step: AgentStep }) {
const [expanded, setExpanded] = useState(false);
switch (step.type) {
case 'thinking':
return (
<div className="flex items-center gap-2 text-sm text-gray-500 py-1">
{step.status === 'running' ? (
<span className="animate-spin">⚙️</span>
) : (
<span>💭</span>
)}
<span>{step.status === 'running' ? '正在思考...' : '思考完成'}</span>
</div>
);
case 'tool_call':
return (
<div className="border rounded-lg overflow-hidden">
<button
className="w-full px-3 py-2 flex items-center justify-between bg-gray-50
hover:bg-gray-100 transition-colors text-sm"
onClick={() => setExpanded(!expanded)}
>
<div className="flex items-center gap-2">
{step.status === 'running' ? (
<span className="animate-pulse">🔧</span>
) : step.status === 'error' ? (
<span>❌</span>
) : (
<span>✅</span>
)}
<span className="font-medium">{step.toolName}</span>
{step.duration != null && (
<span className="text-xs text-gray-400">{step.duration}ms</span>
)}
</div>
<span className="text-xs">{expanded ? '▼' : '▶'}</span>
</button>
{expanded && (
<div className="px-3 py-2 border-t bg-white text-xs">
{/* 工具参数 */}
<div className="mb-2">
<span className="text-gray-500">参数:</span>
<pre className="mt-1 bg-gray-50 p-2 rounded overflow-x-auto">
{JSON.stringify(step.toolArgs, null, 2)}
</pre>
</div>
{/* 工具返回结果 */}
{step.result && (
<div>
<span className="text-gray-500">结果:</span>
<pre className="mt-1 bg-gray-50 p-2 rounded overflow-x-auto max-h-40 overflow-y-auto">
{formatToolResult(step.result)}
</pre>
</div>
)}
</div>
)}
</div>
);
case 'answer':
return (
<div className="prose prose-sm">
<StreamMarkdown content={step.result || ''} isStreaming={step.status === 'running'} />
</div>
);
default:
return null;
}
}
// 格式化工具结果(截断过长内容)
function formatToolResult(result: string): string {
try {
const parsed = JSON.parse(result);
const formatted = JSON.stringify(parsed, null, 2);
return formatted.length > 2000 ? formatted.slice(0, 2000) + '\n...(truncated)' : formatted;
} catch {
return result.length > 2000 ? result.slice(0, 2000) + '...(truncated)' : result;
}
}
五、流式 Agent 步骤(SSE 传输)
Agent 的多步执行需要通过流式传输实时推送给前端:
export async function POST(request: Request): Promise<Response> {
const { messages, tools } = await request.json();
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
// 辅助函数:发送 SSE 事件
const sendEvent = async (event: string, data: unknown) => {
await writer.write(
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
);
};
(async () => {
try {
const result = await agentLoop(messages, tools, {
maxIterations: 10,
maxToolCalls: 5,
timeout: 30000,
totalTimeout: 120000,
// 每个步骤实时推送给前端
onStep: async (step) => {
await sendEvent('agent_step', step);
},
});
// 发送最终结果
await sendEvent('agent_done', {
content: result.content,
totalTokens: result.totalTokens,
stepCount: result.steps.length,
});
} catch (error) {
await sendEvent('agent_error', { error: (error as Error).message });
} finally {
await writer.close();
}
})();
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
});
}
// 前端消费 Agent SSE 流
export function useAgent() {
const [steps, setSteps] = useState<AgentStep[]>([]);
const [isRunning, setIsRunning] = useState(false);
const [finalAnswer, setFinalAnswer] = useState('');
const run = useCallback(async (messages: Message[]) => {
setIsRunning(true);
setSteps([]);
setFinalAnswer('');
const response = await fetch('/api/agent', {
method: 'POST',
body: JSON.stringify({ messages }),
});
for await (const line of readSSELines(response)) {
if (line.startsWith('event: ')) continue;
if (!line.startsWith('data: ')) continue;
const data = JSON.parse(line.slice(6));
if (data.type) {
// agent_step 事件
setSteps(prev => {
const existing = prev.find(s => s.id === data.id);
if (existing) {
return prev.map(s => (s.id === data.id ? { ...s, ...data } : s));
}
return [...prev, data];
});
} else if (data.content) {
// agent_done 事件
setFinalAnswer(data.content);
}
}
setIsRunning(false);
}, []);
return { steps, isRunning, finalAnswer, run };
}
六、Agent 安全与治理
// Agent 安全配置
interface AgentSafetyConfig {
maxIterations: number; // 最大循环次数(防死循环)
maxToolCalls: number; // 单次 LLM 返回最多处理几个工具调用
allowedTools: string[]; // 工具白名单
toolTimeout: number; // 单个工具执行超时 (ms)
totalTimeout: number; // Agent 总超时 (ms)
maxTokenBudget: number; // Token 预算上限
sensitiveFields: string[]; // 需要从工具结果中脱敏的字段
requireConfirmation: string[]; // 需要用户确认的工具列表
}
const DEFAULT_SAFETY: AgentSafetyConfig = {
maxIterations: 10,
maxToolCalls: 5,
allowedTools: ['get_weather', 'web_search', 'query_database', 'run_code'],
toolTimeout: 30_000,
totalTimeout: 120_000,
maxTokenBudget: 50_000,
sensitiveFields: ['password', 'token', 'secret', 'api_key', 'credit_card'],
requireConfirmation: ['send_email', 'delete_record', 'make_payment'],
};
// 安全中间件:验证工具调用合法性
function validateToolCall(
toolCall: UnifiedToolCall,
config: AgentSafetyConfig
): { allowed: boolean; reason?: string } {
// 1. 白名单检查
if (!config.allowedTools.includes(toolCall.name)) {
return { allowed: false, reason: `Tool "${toolCall.name}" is not in the allowed list` };
}
// 2. 参数脱敏检查
const argsStr = JSON.stringify(toolCall.arguments).toLowerCase();
for (const field of config.sensitiveFields) {
if (argsStr.includes(field)) {
return { allowed: false, reason: `Tool arguments contain sensitive field: ${field}` };
}
}
// 3. 需要用户确认的工具
if (config.requireConfirmation.includes(toolCall.name)) {
return { allowed: false, reason: `Tool "${toolCall.name}" requires user confirmation` };
}
return { allowed: true };
}
// 工具结果脱敏
function sanitizeToolResult(result: string, sensitiveFields: string[]): string {
let sanitized = result;
for (const field of sensitiveFields) {
// 匹配 JSON 中的敏感字段值
const regex = new RegExp(`"${field}"\\s*:\\s*"[^"]*"`, 'gi');
sanitized = sanitized.replace(regex, `"${field}": "***REDACTED***"`);
}
return sanitized;
}
七、tool_choice 策略
tool_choice 控制 LLM 是否以及如何调用工具:
| tool_choice 值 | 含义 | 使用场景 |
|---|---|---|
"auto" | LLM 自行决定(默认) | 大多数场景 |
"none" | 禁止调用任何工具 | 只需要对话不需要工具 |
"required" | 必须调用至少一个工具 | 确保拿到结构化数据 |
{ type: "function", function: { name: "xxx" } } | 强制调用指定工具 | 确定性场景(如意图分类) |
// 场景:用 LLM 做意图分类,强制调用分类工具
const classifyResponse = await callLLM({
messages: [{ role: 'user', content: userInput }],
tools: [intentClassifyTool],
tool_choice: { type: 'function', function: { name: 'classify_intent' } },
});
// LLM 一定会返回 tool_calls,不会直接回答
// 场景:聊天模式下禁止工具调用
const chatResponse = await callLLM({
messages: chatHistory,
tools: allTools,
tool_choice: 'none', // 只聊天,不调工具
});
常见面试问题
Q1: Function Calling 的完整执行流程是什么?
答案:
6 个步骤:
- 定义工具:用 JSON Schema 描述每个工具的名称、用途和参数格式
- 发送请求:将用户消息 + 工具定义一起发给 LLM
- 模型决策:LLM 分析用户意图,决定是直接回答还是调用工具。如果需要工具,返回
tool_calls(工具名 + 参数 JSON) - 执行工具:你的后端代码执行对应的函数(调 API、查数据库等)
- 回传结果:将工具结果追加到消息列表,再次发给 LLM
- 生成回答:LLM 基于工具结果生成自然语言回答。如果还需要更多信息,重复 3-5 步
关键理解:LLM 不直接执行任何操作,它只输出 JSON 描述「想做什么」,实际执行在你的代码中,你拥有完全的控制权。
Q2: Agent 和普通 Function Calling 的区别?
答案:
| 维度 | 单次 Function Calling | Agent |
|---|---|---|
| 循环次数 | 1 次(调工具 → 回答) | 多次循环直到完成 |
| 自主性 | 被动:用户问 → 调工具 → 答 | 主动:分解任务、规划步骤 |
| 工具组合 | 通常调 1 个工具 | 多工具协作(先搜索再计算再写入) |
| 复杂度 | 简单直接 | 需要循环控制、超时、安全限制 |
| 典型场景 | "北京天气如何" | "分析上周销售数据并生成报告" |
Agent 的核心是 循环:LLM 可以多次推理,每次决定下一步做什么,直到认为任务完成。
Q3: 如何防止 Agent 进入死循环?
答案:
4 层防护:
- maxIterations:最大循环次数(通常 5-15),超过强制终止
- totalTimeout:总执行时间限制(通常 60-120 秒)
- Token 预算:累计 Token 消耗超过阈值终止(防止无限消耗)
- 重复检测:如果 LLM 连续调用同一个工具且参数相同,说明陷入循环,应终止
终止时不应该返回空结果,而是返回「已超时/超限,以下是已收集的信息」+ 已有的工具结果摘要。
Q4: 工具的 description 为什么很重要?怎么写好?
答案:
description 是 LLM 判断「何时调用」的唯一依据。写不好会导致:
- 该调用时不调用(描述不够精确)
- 不该调用时调用(描述与其他工具重叠)
- 调错工具(多个工具描述相似)
好的 description 包含三要素:
- 功能说明:工具做什么
- 使用场景:什么情况下应该调用
- 约束说明:限制条件
示例:"获取指定城市的当前天气和未来7天预报。当用户询问天气、温度、降雨概率或是否需要带伞等问题时调用。只支持国内主要城市。"
Q5: OpenAI 和 Anthropic 的工具调用有什么结构差异?
答案:
最大差异在于工具结果的传回方式:
-
OpenAI:工具结果用独立的
tool角色消息返回{ "role": "tool", "tool_call_id": "call_xxx", "content": "结果" } -
Anthropic:工具结果放在
user消息的tool_resultcontent block 中{ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "toolu_xxx", "content": "结果" }] }
这意味着适配层需要根据 Provider 不同,构建不同格式的消息。
Q6: 什么是 tool_choice?有哪些策略?
答案:
tool_choice 控制 LLM 是否以及如何选择工具:
"auto"(默认):LLM 自行决定是否调用,适合大多数场景"none":禁止调用工具,强制只输出文本"required":必须调用至少一个工具(不能直接文本回答)- 指定工具名:强制调用特定工具,适合确定性场景(如意图分类、数据提取)
实际应用中,可以通过 tool_choice 实现「模式切换」:聊天模式用 none,Agent 模式用 auto。
Q7: 并行工具调用是什么?如何处理?
答案:
LLM 可以在一次响应中返回多个 tool_calls(如同时查天气和搜索新闻)。处理要点:
- 用
Promise.allSettled()并行执行所有工具 - 部分工具失败不影响其他工具的结果
- 将所有结果(包括错误信息)一起传回 LLM
- 设置
maxToolCalls限制单次最多执行几个工具
const results = await Promise.allSettled(
toolCalls.slice(0, maxToolCalls).map(tc => executeWithTimeout(tc, timeout))
);
Q8: 前端如何实时展示 Agent 的多步执行过程?
答案:
通过 SSE 事件流实时推送每个步骤:
- 后端 Agent 循环中,每个步骤(思考、工具调用开始、工具执行完成)通过
sendEvent()推送 - 前端监听 SSE 事件,逐步渲染步骤列表
- UI 展示:思考步骤(可折叠)→ 工具调用(名称+状态+耗时)→ 工具结果(折叠显示)→ 最终回答
关键体验:工具调用状态用 loading 动画,完成后显示 ✅ 和耗时。让用户清楚地看到 Agent 正在做什么。
Q9: Agent 安全需要注意什么?
答案:
5 个安全层面:
- 工具白名单:只允许调用预定义的工具,防止 Prompt 注入导致调用危险操作
- 参数校验:用 Zod 等库校验工具参数的类型和范围
- 敏感操作确认:删除、发送邮件、支付等操作需要用户二次确认
- 结果脱敏:工具返回的数据中可能包含敏感信息(密码、Token),需要在传回 LLM 前脱敏
- 资源限制:执行超时、Token 预算、循环次数上限
Q10: Zod 在 Function Calling 中有什么作用?
答案:
Zod 的双重作用:
- 生成 JSON Schema:通过
zodToJsonSchema()将 Zod schema 转为 JSON Schema,传给 LLM 描述工具参数 - 运行时校验:用
schema.parse(args)校验 LLM 返回的参数,确保类型正确
这比手写 JSON Schema 更安全——如果 LLM 返回了不符合预期的参数(比如 number 类型传了 string),Zod 会在执行工具前就抛出错误,而不是让错误参数传到下游。
Q11: 如何设计支持「需要用户确认」的工具?
答案:
对于危险操作(删除数据、发邮件、支付),Agent 不应该自动执行,而是暂停等待用户确认:
- 工具调用到达时,检查是否在
requireConfirmation列表中 - 如果需要确认,向前端发送一个
confirmation_required事件 - 前端展示确认对话框(显示工具名、参数、预期效果)
- 用户确认后,前端发送确认信号,后端继续执行
- 用户拒绝则将「用户拒绝了此操作」作为工具结果返回给 LLM
Q12: Agent 的 Token 消耗为什么远高于普通对话?
答案:
Agent 每次循环都是一次完整的 LLM 调用:
- 第 1 次:用户消息 + 工具定义(工具定义本身消耗大量 token)
- 第 2 次:上一次全部消息 + 工具结果 + 工具定义
- 第 N 次:消息列表越来越长
假设工具定义 500 tokens、每次对话 200 tokens、工具结果平均 300 tokens:
- 3 步 Agent:~(500+200) + (500+400+300) + (500+700+600) ≈ 4,200 tokens input
控制成本的方法:
- 精简工具 description(只提供当前可能用到的工具)
- 压缩工具结果(截断、只返回关键字段)
- 设置 Token 预算上限
- 使用小模型做简单工具路由,大模型做最终回答
相关链接
- OpenAI Function Calling
- Anthropic Tool Use
- 前端接入大模型 API - Function Calling API 格式详解
- MCP 协议 - 标准化的工具调用协议
- AI SDK 与框架 - Vercel AI SDK 的 tool 系统
- AI 应用安全 - Prompt 注入与工具安全