跳到主要内容

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)的实现:

  1. Think:LLM 分析当前情况和目标
  2. Act:LLM 决定调用哪个工具,输出工具名和参数
  3. Observe:工具执行结果返回给 LLM
  4. 重复直到 LLM 认为可以回答用户问题

Agent 核心代码

lib/agent-loop.ts
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 能准确判断「何时调用」+「传什么参数」。

tools/definitions.ts
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 工具调用差异

差异点OpenAIAnthropic
工具定义tools[].function.parameterstools[].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
lib/provider-adapter.ts
// 统一适配层:将不同 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 的执行过程——每个工具调用的状态、耗时、结果。

components/AgentSteps.tsx
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 的多步执行需要通过流式传输实时推送给前端:

app/api/agent/route.ts
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',
},
});
}
hooks/useAgent.ts
// 前端消费 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 安全与治理

lib/agent-safety.ts
// 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" } }强制调用指定工具确定性场景(如意图分类)
examples/tool-choice.ts
// 场景:用 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 个步骤:

  1. 定义工具:用 JSON Schema 描述每个工具的名称、用途和参数格式
  2. 发送请求:将用户消息 + 工具定义一起发给 LLM
  3. 模型决策:LLM 分析用户意图,决定是直接回答还是调用工具。如果需要工具,返回 tool_calls(工具名 + 参数 JSON)
  4. 执行工具:你的后端代码执行对应的函数(调 API、查数据库等)
  5. 回传结果:将工具结果追加到消息列表,再次发给 LLM
  6. 生成回答:LLM 基于工具结果生成自然语言回答。如果还需要更多信息,重复 3-5 步

关键理解:LLM 不直接执行任何操作,它只输出 JSON 描述「想做什么」,实际执行在你的代码中,你拥有完全的控制权。

Q2: Agent 和普通 Function Calling 的区别?

答案

维度单次 Function CallingAgent
循环次数1 次(调工具 → 回答)多次循环直到完成
自主性被动:用户问 → 调工具 → 答主动:分解任务、规划步骤
工具组合通常调 1 个工具多工具协作(先搜索再计算再写入)
复杂度简单直接需要循环控制、超时、安全限制
典型场景"北京天气如何""分析上周销售数据并生成报告"

Agent 的核心是 循环:LLM 可以多次推理,每次决定下一步做什么,直到认为任务完成。

Q3: 如何防止 Agent 进入死循环?

答案

4 层防护:

  1. maxIterations:最大循环次数(通常 5-15),超过强制终止
  2. totalTimeout:总执行时间限制(通常 60-120 秒)
  3. Token 预算:累计 Token 消耗超过阈值终止(防止无限消耗)
  4. 重复检测:如果 LLM 连续调用同一个工具且参数相同,说明陷入循环,应终止

终止时不应该返回空结果,而是返回「已超时/超限,以下是已收集的信息」+ 已有的工具结果摘要。

Q4: 工具的 description 为什么很重要?怎么写好?

答案

description 是 LLM 判断「何时调用」的唯一依据。写不好会导致:

  • 该调用时不调用(描述不够精确)
  • 不该调用时调用(描述与其他工具重叠)
  • 调错工具(多个工具描述相似)

好的 description 包含三要素:

  1. 功能说明:工具做什么
  2. 使用场景:什么情况下应该调用
  3. 约束说明:限制条件

示例:"获取指定城市的当前天气和未来7天预报。当用户询问天气、温度、降雨概率或是否需要带伞等问题时调用。只支持国内主要城市。"

Q5: OpenAI 和 Anthropic 的工具调用有什么结构差异?

答案

最大差异在于工具结果的传回方式

  • OpenAI:工具结果用独立的 tool 角色消息返回

    { "role": "tool", "tool_call_id": "call_xxx", "content": "结果" }
  • Anthropic:工具结果放在 user 消息的 tool_result content 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(如同时查天气和搜索新闻)。处理要点:

  1. Promise.allSettled() 并行执行所有工具
  2. 部分工具失败不影响其他工具的结果
  3. 将所有结果(包括错误信息)一起传回 LLM
  4. 设置 maxToolCalls 限制单次最多执行几个工具
const results = await Promise.allSettled(
toolCalls.slice(0, maxToolCalls).map(tc => executeWithTimeout(tc, timeout))
);

Q8: 前端如何实时展示 Agent 的多步执行过程?

答案

通过 SSE 事件流实时推送每个步骤:

  1. 后端 Agent 循环中,每个步骤(思考、工具调用开始、工具执行完成)通过 sendEvent() 推送
  2. 前端监听 SSE 事件,逐步渲染步骤列表
  3. UI 展示:思考步骤(可折叠)→ 工具调用(名称+状态+耗时)→ 工具结果(折叠显示)→ 最终回答

关键体验:工具调用状态用 loading 动画,完成后显示 ✅ 和耗时。让用户清楚地看到 Agent 正在做什么。

Q9: Agent 安全需要注意什么?

答案

5 个安全层面:

  1. 工具白名单:只允许调用预定义的工具,防止 Prompt 注入导致调用危险操作
  2. 参数校验:用 Zod 等库校验工具参数的类型和范围
  3. 敏感操作确认:删除、发送邮件、支付等操作需要用户二次确认
  4. 结果脱敏:工具返回的数据中可能包含敏感信息(密码、Token),需要在传回 LLM 前脱敏
  5. 资源限制:执行超时、Token 预算、循环次数上限

Q10: Zod 在 Function Calling 中有什么作用?

答案

Zod 的双重作用:

  1. 生成 JSON Schema:通过 zodToJsonSchema() 将 Zod schema 转为 JSON Schema,传给 LLM 描述工具参数
  2. 运行时校验:用 schema.parse(args) 校验 LLM 返回的参数,确保类型正确

这比手写 JSON Schema 更安全——如果 LLM 返回了不符合预期的参数(比如 number 类型传了 string),Zod 会在执行工具前就抛出错误,而不是让错误参数传到下游。

Q11: 如何设计支持「需要用户确认」的工具?

答案

对于危险操作(删除数据、发邮件、支付),Agent 不应该自动执行,而是暂停等待用户确认:

  1. 工具调用到达时,检查是否在 requireConfirmation 列表中
  2. 如果需要确认,向前端发送一个 confirmation_required 事件
  3. 前端展示确认对话框(显示工具名、参数、预期效果)
  4. 用户确认后,前端发送确认信号,后端继续执行
  5. 用户拒绝则将「用户拒绝了此操作」作为工具结果返回给 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

控制成本的方法:

  1. 精简工具 description(只提供当前可能用到的工具)
  2. 压缩工具结果(截断、只返回关键字段)
  3. 设置 Token 预算上限
  4. 使用小模型做简单工具路由,大模型做最终回答

相关链接