2026 年,AI Agent 已经从概念验证走向生产部署。据统计,超过 60% 的企业级 AI 应用采用 Agent 架构而非简单的 Chatbot,但其中近 40% 的项目在上线后遭遇了工具调用失败率高、上下文溢出、幻觉循环等问题。这篇文章不是教你调 API,而是从工程视角拆解一个能在生产环境稳定运行的 AI Agent 到底该怎么构建——包括架构设计、工具调用协议、记忆管理和错误恢复机制。
🏗️ 一、Agent 核心架构:不只是 while 循环调 LLM
大多数开发者对 AI Agent 的理解停留在"LLM + 工具调用的 while 循环",这在 demo 阶段够用,但生产环境会遇到一系列工程问题。一个成熟的 Agent 架构需要包含以下核心模块:
1.1 三层架构设计
生产级 Agent 通常采用三层架构:
| 层级 | 职责 | 关键技术点 |
|---|---|---|
| 编排层(Orchestrator) | 控制 Agent 循环、决定何时终止、管理并发子 Agent | 状态机、最大轮次限制、超时控制 |
| 推理层(Reasoning) | LLM 调用、Prompt 构建、响应解析 | 流式输出、结构化输出、Token 计数 |
| 执行层(Execution) | 工具调用、结果序列化、错误处理 | 并发执行、超时、沙箱隔离 |
📌 **记住:**编排层是 Agent 的大脑,推理层是思考能力,执行层是手脚。三层解耦才能独立迭代和监控。
1.2 从零实现最小 Agent 内核
以下是一个最小但完整的 Agent 内核实现,支持 Function Calling 和最大轮次限制:
// agent-kernel.ts — 最小 Agent 内核,支持工具调用和轮次控制
interface Message {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string;
tool_call_id?: string;
name?: string;
}
interface Tool {
name: string;
description: string;
parameters: Record<string, unknown>;
execute: (args: Record<string, unknown>) => Promise<string>;
}
interface AgentConfig {
maxIterations: number; // 最大推理轮次,防止无限循环
maxTokens: number; // 单次响应最大 Token
timeoutMs: number; // 单轮超时时间
temperature: number;
}
class Agent {
private tools: Map<string, Tool> = new Map();
private config: AgentConfig;
private llm: LLMClient;
constructor(llm: LLMClient, config: Partial<AgentConfig> = {}) {
this.llm = llm;
this.config = {
maxIterations: 10,
maxTokens: 4096,
timeoutMs: 30_000,
temperature: 0.1,
...config,
};
}
registerTool(tool: Tool): void {
this.tools.set(tool.name, tool);
}
async run(userMessage: string, systemPrompt: string): Promise<string> {
const messages: Message[] = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
];
for (let iteration = 0; iteration < this.config.maxIterations; iteration++) {
// 调用 LLM,传入工具定义
const response = await this.callLLM(messages);
// 如果 LLM 返回纯文本(无工具调用),任务完成
if (!response.tool_calls || response.tool_calls.length === 0) {
return response.content;
}
// 执行所有工具调用(支持并行)
const toolResults = await this.executeTools(response.tool_calls);
// 将工具结果追加到消息历史
messages.push({ role: 'assistant', content: response.content });
for (const result of toolResults) {
messages.push({
role: 'tool',
tool_call_id: result.toolCallId,
content: result.output,
});
}
}
// ⚠️ 达到最大轮次,强制终止
return '[Agent] 已达到最大推理轮次限制,请简化任务后重试。';
}
private async callLLM(messages: Message[]): Promise<LLMResponse> {
const toolDefs = Array.from(this.tools.values()).map(t => ({
type: 'function',
function: { name: t.name, description: t.description, parameters: t.parameters },
}));
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
try {
return await this.llm.chat({
messages,
tools: toolDefs,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}
}
private async executeTools(toolCalls: ToolCall[]): Promise<ToolResult[]> {
// 并发执行所有工具调用,单个失败不影响其他
const results = await Promise.allSettled(
toolCalls.map(async (tc) => {
const tool = this.tools.get(tc.function.name);
if (!tool) {
return { toolCallId: tc.id, output: `错误:未知工具 ${tc.function.name}` };
}
const args = JSON.parse(tc.function.arguments);
const output = await tool.execute(args);
return { toolCallId: tc.id, output };
})
);
return results.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
return { toolCallId: toolCalls[i].id, output: `工具执行失败: ${r.reason}` };
});
}
}
💡 **提示:**注意
maxIterations的设置——生产环境建议 5-15 次。太小会限制复杂任务,太大会浪费 Token 甚至导致 Agent 在错误路径上越走越远。
1.3 Agent 循环的三个致命陷阱
❌ 陷阱 1:无最大轮次限制 — Agent 在工具调用失败时可能反复重试,产生无限循环,Token 费用爆炸。
❌ 陷阱 2:工具结果直接拼入 Prompt — 如果工具返回超大结果(如数据库查询返回 10 万行),直接塞入上下文会导致 Token 溢出。
❌ 陷阱 3:不做 Token 预算管理 — 多轮对话后上下文超出模型窗口,LLM 会截断早期消息,丢失关键工具定义。
🔧 二、工具调用系统:协议设计与错误处理
工具调用是 Agent 的核心能力,但也是最容易出问题的环节。一个设计良好的工具调用系统需要解决三个问题:参数校验、执行隔离、结果压缩。
2.1 工具定义与参数校验
LLM 返回的工具参数是 JSON 字符串,直接 JSON.parse 后使用是危险的。必须做严格的 Schema 校验:
// tool-schema.ts — 使用 Zod 做工具参数校验
import { z } from 'zod';
// 定义工具的参数 Schema
const searchWebSchema = z.object({
query: z.string().min(1).max(500),
maxResults: z.number().int().min(1).max(20).default(5),
language: z.enum(['zh', 'en']).default('zh'),
});
// 创建工具时绑定 Schema
function createTool<T extends z.ZodType>(
name: string,
description: string,
schema: T,
handler: (args: z.infer<T>) => Promise<string>
): Tool {
return {
name,
description,
parameters: zodToJsonSchema(schema), // 转为 JSON Schema 供 LLM 理解
execute: async (rawArgs: Record<string, unknown>) => {
// ⚡ 关键:先校验再执行
const result = schema.safeParse(rawArgs);
if (!result.success) {
// 返回校验错误信息,让 LLM 自行修正参数
return `参数错误: ${result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ')}`;
}
return handler(result.data);
},
};
}
// 使用示例
const searchTool = createTool(
'search_web',
'搜索互联网获取最新信息',
searchWebSchema,
async ({ query, maxResults, language }) => {
const results = await fetch(`https://api.search.example/q=${encodeURIComponent(query)}&limit=${maxResults}&lang=${language}`);
const data = await results.json();
return JSON.stringify(data.items.slice(0, maxResults));
}
);
⚠️ **警告:**永远不要信任 LLM 返回的参数格式。我见过 Agent 调用文件删除工具时传入
/作为路径——参数校验是你的安全底线。
2.2 工具执行的并发控制与超时
生产环境中,工具可能是外部 API 调用、数据库查询或文件操作,必须做并发控制和超时管理:
| 策略 | 适用场景 | 实现方式 |
|---|---|---|
| 串行执行 | 有依赖关系的工具链 | for...of + await |
| 完全并行 | 无依赖的独立工具 | Promise.all |
| 受限并行 | 控制外部 API 并发数 | p-limit 或自定义 Semaphore |
| 超时熔断 | 防止单个工具卡死 | AbortController + Promise.race |
// tool-executor.ts — 带超时和并发控制的工具执行器
class ToolExecutor {
private semaphore: number;
private queue: Array<() => void> = [];
private running = 0;
constructor(private maxConcurrency: number = 3) {
this.semaphore = maxConcurrency;
}
async executeWithTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number = 10_000
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`工具执行超时 (${timeoutMs}ms)`));
}, timeoutMs);
fn().then(
(result) => { clearTimeout(timer); resolve(result); },
(err) => { clearTimeout(timer); reject(err); }
);
});
}
async executeLimited<T>(fn: () => Promise<T>, timeoutMs?: number): Promise<T> {
// 等待获取执行许可
await this.acquire();
try {
return timeoutMs
? await this.executeWithTimeout(fn, timeoutMs)
: await fn();
} finally {
this.release();
}
}
private acquire(): Promise<void> {
if (this.semaphore > 0) {
this.semaphore--;
return Promise.resolve();
}
return new Promise<void>((resolve) => {
this.queue.push(resolve);
});
}
private release(): void {
const next = this.queue.shift();
if (next) {
next();
} else {
this.semaphore++;
}
}
}
2.3 工具结果压缩与 Token 预算
当工具返回大量数据时,直接塞入上下文是灾难性的。必须做结果压缩:
- ✅ 截断策略:保留前 N 条结果 + 总数提示(如"共 1000 条,显示前 10 条")
- ✅ 摘要策略:对长文本用 LLM 做摘要后再放入上下文
- ✅ 结构化提取:只提取 LLM 需要的字段,去掉冗余数据
- ❌ 避免:把完整 API 响应(含 HTTP headers、status 等)直接传给 LLM
🧠 三、记忆系统与上下文管理
Agent 的记忆系统决定了它能否处理复杂的多步任务。简单地把所有历史消息拼接在一起,很快就会超出上下文窗口。
3.1 三层记忆架构
| 记忆层级 | 类比 | 生命周期 | 存储方式 |
|---|---|---|---|
| 工作记忆 | 人的短期记忆 | 单次任务 | 消息数组,滑动窗口 |
| 情景记忆 | 人的经历记忆 | 跨会话 | 向量数据库,按相关性检索 |
| 程序记忆 | 人的肌肉记忆 | 永久 | 工具定义 + 系统 Prompt |
⚡ **关键结论:**工作记忆管当下对话,情景记忆管历史经验,程序记忆管"我会什么"。三层配合才能让 Agent 处理复杂任务。
3.2 上下文窗口管理实现
// context-manager.ts — 智能上下文窗口管理
interface ContextBudget {
totalTokens: number; // 模型上下文窗口大小
reserveForResponse: number; // 预留给响应的 Token
systemPromptTokens: number; // 系统 Prompt 占用
toolsTokens: number; // 工具定义占用
}
class ContextManager {
private budget: ContextBudget;
private tokenCounter: TokenCounter;
constructor(model: string) {
this.tokenCounter = new TokenCounter(model);
// 根据模型设置不同的上下文预算
this.budget = this.getBudget(model);
}
private getBudget(model: string): ContextBudget {
const budgets: Record<string, ContextBudget> = {
'gpt-4o': { totalTokens: 128_000, reserveForResponse: 4096, systemPromptTokens: 0, toolsTokens: 0 },
'claude-sonnet-4-20250514': { totalTokens: 200_000, reserveForResponse: 8192, systemPromptTokens: 0, toolsTokens: 0 },
};
return budgets[model] || budgets['gpt-4o'];
}
// 获取可用于消息的 Token 预算
get availableTokens(): number {
return this.budget.totalTokens
- this.budget.reserveForResponse
- this.budget.systemPromptTokens
- this.budget.toolsTokens;
}
// 裁剪消息历史,保留关键上下文
trimMessages(messages: Message[]): Message[] {
let totalTokens = 0;
const systemMessages: Message[] = [];
const conversationMessages: Message[] = [];
for (const msg of messages) {
if (msg.role === 'system') {
systemMessages.push(msg);
} else {
conversationMessages.push(msg);
}
}
// 从最新消息向前遍历,直到 Token 预算用完
const trimmed: Message[] = [];
for (let i = conversationMessages.length - 1; i >= 0; i--) {
const msgTokens = this.tokenCounter.count(conversationMessages[i].content);
if (totalTokens + msgTokens > this.availableTokens) {
// 在截断处插入提示
trimmed.unshift({
role: 'system',
content: `[注意:以上为较早的历史记录,已被截断以节省上下文空间]`,
});
break;
}
totalTokens += msgTokens;
trimmed.unshift(conversationMessages[i]);
}
return [...systemMessages, ...trimmed];
}
}
3.3 避免上下文污染的最佳实践
在实际开发中,上下文污染是 Agent 失败的常见原因:
- ✅ 工具结果隔离:每个工具调用结果用明确的分隔符包裹,防止 LLM 混淆工具输出和用户输入
- ✅ 敏感信息脱敏:工具可能返回包含密码、Token 的日志,需要在放入上下文前过滤
- ✅ 错误信息降级:工具执行失败时,只返回错误类型和建议,不返回完整堆栈
- ❌ 避免:把整个 JSON 文件(数千行)作为工具结果返回给 LLM
📊 四、生产部署的可靠性工程
Agent 在生产环境中面临的核心挑战是可靠性。LLM 的输出天然具有不确定性,工具调用也可能失败,必须建立完整的错误恢复机制。
4.1 错误分级与重试策略
| 错误类型 | 示例 | 重试策略 | 最大重试 |
|---|---|---|---|
| LLM 限流 | 429 Too Many Requests | 指数退避 | 3 次 |
| 工具执行失败 | API 超时、网络错误 | 告诉 LLM 失败原因,让它决定是否重试 | 由 LLM 决策 |
| 参数校验失败 | LLM 传了错误参数 | 返回校验错误,LLM 自动修正 | 3 次 |
| 上下文溢出 | Token 超限 | 截断历史 + 重试 | 1 次 |
| 幻觉循环 | LLM 反复调用不存在的工具 | 记录工具名黑名单,强制终止 | 0 次 |
⚠️ **警告:**幻觉循环是最危险的故障模式——LLM 反复调用一个不存在的工具,每次都失败,每次都重试。必须设置"未知工具调用计数器",超过阈值立即终止。
4.2 可观测性:Agent 的日志与监控
生产 Agent 必须记录每一次推理循环的完整状态:
// agent-logger.ts — Agent 可观测性日志
interface AgentStepLog {
iteration: number;
timestamp: number;
llmTokensUsed: { input: number; output: number };
toolCalls: Array<{
name: string;
args: string;
resultPreview: string; // 截断到 200 字符
durationMs: number;
success: boolean;
error?: string;
}>;
decision: 'continue' | 'finish' | 'error' | 'max_iterations';
totalDurationMs: number;
}
class AgentLogger {
private steps: AgentStepLog[] = [];
private startTime = Date.now();
logStep(step: AgentStepLog): void {
this.steps.push(step);
// 实时输出到监控系统
console.log(JSON.stringify({
event: 'agent_step',
iteration: step.iteration,
tools: step.toolCalls.map(t => `${t.name}(${t.success ? 'ok' : 'fail'})`),
tokens: step.llmTokensUsed,
duration: step.totalDurationMs,
decision: step.decision,
}));
}
getSummary(): AgentSummary {
const totalTokens = this.steps.reduce(
(acc, s) => acc + s.llmTokensUsed.input + s.llmTokensUsed.output, 0
);
const totalTools = this.steps.reduce((acc, s) => acc + s.toolCalls.length, 0);
const failedTools = this.steps.reduce(
(acc, s) => acc + s.toolCalls.filter(t => !t.success).length, 0
);
return {
totalIterations: this.steps.length,
totalTokens,
totalToolCalls: totalTools,
failedToolCalls: failedTools,
toolSuccessRate: totalTools > 0 ? (totalTools - failedTools) / totalTools : 1,
totalDurationMs: Date.now() - this.startTime,
};
}
}
✅ 总结与实践建议
构建生产级 AI Agent 不是一个 LLM API 调用问题,而是一个系统工程问题。以下是核心要点:
- ✅ 架构解耦:编排层、推理层、执行层分离,方便独立测试和监控
- ✅ 工具参数校验:永远用 Zod/Joi 等 Schema 库校验 LLM 返回的参数
- ✅ Token 预算管理:计算系统 Prompt + 工具定义 + 历史消息的 Token 总量,预留响应空间
- ✅ 错误恢复:区分可重试错误和致命错误,让 LLM 自行决定是否重试工具调用
- ✅ 可观测性:记录每轮推理的 Token 消耗、工具调用结果、决策原因
- ❌ 避免无限循环:必须设置
maxIterations,这是 Agent 的"死人开关" - ❌ 避免盲目信任:LLM 返回的参数、工具名、执行顺序都需要校验
⚡ **关键结论:**一个好的 Agent 不是"能跑通 demo"的 Agent,而是"在工具失败、网络超时、上下文溢出时依然能优雅降级"的 Agent。工程可靠性比推理能力更重要。
相关工具推荐:
- 🔧 Vercel AI SDK — 提供统一的 LLM 调用接口和流式输出支持
- 🔧 Zod — TypeScript-first 的参数校验库,与 JSON Schema 无缝转换
- 🔧 tiktoken / js-tiktoken — 精确的 Token 计数,用于上下文预算管理
- 🔧 LangSmith / Langfuse — Agent 可观测性平台,追踪每次工具调用和推理决策