AI Agent 的软件架构:计划、工具、记忆和权限
Agent 不是一个大 Prompt
去年我在一个内部工具项目里,把整个 Agent 的能力写在一个 3000 行的 System Prompt 里——计划、工具调用、记忆管理、权限判断全混在一起。上线第一周,它在 80% 的简单任务上表现惊人;第二周,用户开始报告它在复杂任务里「忘记」之前的步骤、调用不该调的工具、甚至编造执行结果。
问题的根源不是模型能力不够,而是架构上的混乱:所有职责耦合在一起,没有清晰的模块边界,出了问题无法定位,也无法单独替换某个模块。这和我过去做分布式系统时的经验完全一致——当一个服务承担了太多职责,它一定会在某些边界条件下失控。
Agent 架构本质上是一个软件工程问题。LLM 提供了推理能力,但围绕它的计划、工具、记忆、权限、状态和监控模块,需要用经典的软件工程方法来设计。Google Cloud 在 2026 年发布的 Agentic AI 架构指南中明确建议:「如果你刚开始 Agent 开发,从单 Agent 开始,聚焦核心逻辑,再逐步增加复杂度。」1 这句话的重点不在「单 Agent」,而在「逐步增加复杂度」——每一步增加都应该是架构上可控的。
这篇文章来自我过去一年在生产环境中设计和调试 Agent 系统的实际经验。我会用具体的案例、代码和对比表格,拆解 Agent 架构中最关键的六个模块,以及它们之间的协作方式。
核心架构:六个模块的职责边界
一个生产级 Agent 系统可以拆成六个独立模块,每个模块有明确的输入输出和职责边界:
| 模块 | 核心职责 | 输入 | 输出 | 典型失败模式 |
|---|---|---|---|---|
| 计划模块 | 将用户目标分解为可执行步骤 | 用户意图、当前状态 | 步骤列表、执行顺序 | 计划漂移、死循环 |
| 工具模块 | 执行外部操作并返回结果 | 工具名称、参数 | 执行结果、错误信息 | 参数幻觉、静默失败 |
| 记忆模块 | 管理短期和长期上下文 | 对话历史、中间状态 | 压缩后的上下文 | 上下文丢失、记忆泄漏 |
| 权限模块 | 控制工具访问和操作边界 | 工具调用请求、用户角色 | 允许/拒绝/需确认 | 越权操作、过度拦截 |
| 状态机 | 管理执行流程和状态转换 | 当前步骤、执行结果 | 下一步状态、终止条件 | 状态跳跃、无法终止 |
| 可观测性 | 记录全过程并支持回放 | 所有模块的执行事件 | 结构化 trace、指标 | 黑盒运行、无法复盘 |
这些模块之间不应存在隐式依赖。计划模块不应该知道工具的具体实现细节,工具模块不应该关心记忆如何压缩上下文,权限模块应该作为独立的拦截层存在。这种解耦不只是为了代码整洁——它让每个模块可以独立测试、独立替换、独立升级。
这个流程图展示了一个完整的 Agent 执行循环。关键设计决策在于:权限检查在工具执行之前,状态转换在结果回来之后,可观测性作为旁路记录所有事件而不是嵌入业务逻辑。
计划模块:ReAct 和 Plan-and-Execute 的工程取舍
计划模块是 Agent 的「大脑」。目前主流的两种模式是 ReAct(Reasoning and Acting)和 Plan-and-Execute。
ReAct 模式让模型在每一步先推理再行动,观察结果后决定下一步。它适合任务路径不确定的场景——比如一个需要多步搜索的研究助手。Plan-and-Execute 模式让模型先列出完整计划,再逐步执行,适合步骤可预见的结构化任务——比如数据处理流水线。
这两种模式的选择不是一个理论问题,而是一个工程权衡:
| 维度 | ReAct | Plan-and-Execute |
|---|---|---|
| 规划时机 | 每步实时规划 | 开始前一次性规划 |
| 适应性 | 高,可根据中间结果调整 | 低,计划可能在执行中失效 |
| 延迟 | 每步都有推理开销 | 推理集中在计划阶段 |
| 可控性 | 难以预知总步数 | 步骤数可预估 |
| 适用场景 | 搜索、调试、探索性任务 | 数据处理、文档生成、流程自动化 |
| 失败模式 | 推理循环、步骤膨胀 | 计划僵化、无法适应异常 |
我在实际项目中踩过一个坑:一个代码审查 Agent 使用了 Plan-and-Execute 模式,先列出「读代码→分析风格→检查类型→生成建议」四步计划。但在第二步分析风格时发现代码库混用了两种风格,需要回溯到第一步重新读代码——但计划已经不可修改,Agent 只能在错误的假设上继续。
修复方式是将计划模块改为「可修正计划」:当某一步的执行结果与预期不符时,允许计划模块重新规划后续步骤。
// ❌ 坏做法:计划一旦生成不可修改
interface RigidPlan {
steps: PlanStep[] // 生成后不再变化
currentIndex: number
}
function executeRigidPlan(plan: RigidPlan) {
for (const step of plan.steps) {
// 即使上一步结果异常,也硬着头皮继续
const result = await executeStep(step)
plan.currentIndex++
}
}
// ✅ 好做法:计划可根据执行结果修正
interface AdaptivePlan {
steps: PlanStep[]
currentIndex: number
maxReplans: number // 防止无限重规划
replanCount: number
}
async function executeAdaptivePlan(plan: AdaptivePlan, context: ExecutionContext) {
while (plan.currentIndex < plan.steps.length) {
const step = plan.steps[plan.currentIndex]
const result = await executeStep(step)
if (result.anomaly && plan.replanCount < plan.maxReplans) {
// 执行结果与预期不符,重新规划后续步骤
const newSteps = await replanner.replan({
completedSteps: plan.steps.slice(0, plan.currentIndex + 1),
remainingGoal: step.originalGoal,
anomaly: result.anomaly,
})
plan.steps = [...plan.steps.slice(0, plan.currentIndex + 1), ...newSteps]
plan.replanCount++
}
plan.currentIndex++
}
}差异在于:坏做法把计划当作单程票,好做法给计划留了修正窗口。maxReplans 是安全阀——没有它,Agent 可能在异常情况下无限重规划,消耗完所有 token 预算。Manus 团队在他们的上下文工程实践中采用了类似的策略:维护一个 todo.md 文件,持续更新任务状态,让主目标始终保持在上下文的末尾(也就是模型注意力最集中的位置)。2
工具调用:从「能用」到「可控」
工具调用是 Agent 与外部世界交互的接口。一个常见的错误是:给 Agent 注册所有可用工具,然后在 Prompt 里写一句「请合理使用工具」。这在原型阶段可以工作,在生产环境中会制造三种问题:
- 参数幻觉:模型编造不存在的参数值,比如填一个虚构的文件路径
- 越权调用:模型调用了超出当前用户权限的工具
- 静默失败:工具返回错误,模型在下一轮推理中假装调用成功
这三种问题的根源相同:工具调用缺乏结构化的约束和验证。
| 维度 | 无约束工具调用 | 结构化约束 |
|---|---|---|
| 工具注册 | 全量注册,Agent 自由选择 | 按角色和场景动态注册 |
| 参数校验 | 依赖模型遵循 schema | schema 校验 + 业务规则校验 |
| 权限控制 | 无,或仅在 Prompt 中声明 | 独立权限层,调用前拦截 |
| 失败处理 | 依赖模型自行重试 | 结构化错误 + 重试策略 + 兜底 |
| 审计追踪 | 无 | 每次调用记录完整 trace |
我后来对工具模块的设计采用了「三层防护」:
// 第一层:工具注册——按角色过滤可用工具
interface ToolRegistry {
// ❌ 坏做法:返回所有工具
// getTools(): Tool[]
// ✅ 好做法:按角色和场景返回工具子集
getTools(context: { role: string; scenario: string }): Tool[] {
return allTools.filter(tool =>
tool.allowedRoles.includes(context.role) &&
tool.allowedScenarios.includes(context.scenario)
)
}
}
// 第二层:调用前拦截——独立权限检查
interface ToolInterceptor {
async intercept(call: ToolCall, user: UserInfo): Promise<InterceptResult> {
const tool = registry.get(call.name)
// 高风险工具需要二次确认
if (tool.riskLevel === 'high' && !user.hasConfirmed) {
return { allowed: false, reason: '需要用户确认', requireConfirm: true }
}
// 参数校验:不信任模型生成的参数
const validation = tool.schema.safeParse(call.arguments)
if (!validation.success) {
return { allowed: false, reason: `参数校验失败: ${validation.error}` }
}
return { allowed: true }
}
}
// 第三层:调用后验证——不能只看返回码
interface ToolResultVerifier {
async verify(call: ToolCall, result: ToolResult): Promise<VerifiedResult> {
// ❌ 坏做法:直接返回原始结果
// return result
// ✅ 好做法:检查结果是否合理
if (result.status === 'error') {
return { status: 'failed', error: result.error, retryable: result.retryable }
}
// 检查返回内容是否与调用意图一致
if (tool.expectedOutputPattern && !tool.expectedOutputPattern.test(result.output)) {
return { status: 'suspicious', warning: '返回结果格式异常', raw: result.output }
}
return { status: 'success', data: result.output }
}
}这三层设计的核心思路是:不信任模型的任何输出。工具列表要按角色过滤,参数要校验,调用要授权,结果要验证。这不是对模型能力的否定,而是工程上的防御性设计。就像 Web 应用不信任用户输入一样,Agent 系统不信任 LLM 生成的工具调用参数。
LangChain 2026 年的 Agent 工程现状报告显示,近 90% 的生产 Agent 部署了某种形式的监控,但只有不到一半对工具调用做了结构化追踪。3 工具调用是 Agent 最容易出问题的环节,也是投入产出比最高的加固点。
记忆系统:上下文不是越长越好
记忆管理是 Agent 架构中最容易被低估的模块。很多人直觉上认为:模型的上下文窗口够大(128K、200K),把所有信息都塞进去就行了。实际运行中,上下文过长会导致三个问题:
- 注意力衰减:模型对上下文中间部分的关注度明显低于首尾(「Lost in the Middle」现象)
- 推理质量下降:无关信息越多,模型的推理准确率越低
- 成本和延迟线性增长:每个 token 都要钱,每次请求都要等
Manus 团队的解决方案很有启发性:把本地文件系统当作「无限外部大脑」,Agent 按需读写文件,而不是把所有信息都塞进上下文。当需要压缩上下文时,确保关键信息可恢复——比如丢弃网页正文但保留 URL,需要时可以重新抓取。2
| 记忆类型 | 生命周期 | 存储位置 | 容量 | 典型用途 |
|---|---|---|---|---|
| 工作记忆 | 当前步骤 | 上下文窗口内 | 有限(受窗口限制) | 当前推理链、工具参数 |
| 短期记忆 | 当前会话 | 上下文窗口 + 外部存储 | 中等 | 对话历史、中间状态 |
| 长期记忆 | 跨会话 | 向量数据库 / 文件系统 | 大 | 用户偏好、历史经验、知识库 |
// ❌ 坏做法:无限堆积上下文,直到触碰窗口上限
class NaiveMemory {
private messages: Message[] = []
add(message: Message) {
this.messages.push(message) // 只增不减
}
getContext(): Message[] {
return this.messages // 可能已经超出窗口
}
}
// ✅ 好做法:分层记忆 + 主动压缩 + 外部存储
interface MemoryLayer {
working: Message[] // 工作记忆:最近 N 步
shortTerm: Message[] // 短期记忆:当前会话摘要
longTerm: string[] // 长期记忆:外部存储的检索结果
}
class StructuredMemory {
private memory: MemoryLayer = { working: [], shortTerm: [], longTerm: [] }
private workingLimit = 20 // 工作记忆保留最近 20 条
add(message: Message) {
this.memory.working.push(message)
// 工作记忆超出上限时,压缩旧消息到短期记忆
if (this.memory.working.length > this.workingLimit) {
const toCompress = this.memory.working.splice(0, 10)
const summary = this.compressor.summarize(toCompress)
this.memory.shortTerm.push({ role: 'system', content: summary })
}
}
getContext(): Message[] {
return [
...this.memory.shortTerm, // 会话摘要在前
...this.memory.working, // 最近步骤在后(注意力最强区域)
]
}
// 按需从长期记忆检索
async recall(query: string): Promise<string[]> {
return this.vectorStore.search(query, { topK: 5 })
}
}关键差异:坏做法把上下文当成一个只增不减的数组;好做法把记忆分成三层,每层有不同的保留策略,并通过压缩和检索控制上下文的实际大小。
有一个容易忽略的细节:不要删除失败的操作记录。Manus 团队的实践表明,保留错误记录在上下文中,能显著降低模型重复同样错误的概率。错误信息帮助模型更新了内部的因果理解,而成功信息往往只是确认了已有的策略。2
案例:一个多步骤 Agent 的三次翻车与修复
理论讲完,我用一个真实案例把上面的模块串起来。这是一个「自动分析 GitHub Issue 并生成修复 PR」的 Agent。
案例一:计划漂移导致死循环
场景:Issue 描述了一个 CSS 样式问题。Agent 计划:定位文件 → 分析样式 → 修改代码 → 运行测试 → 提交 PR。
翻车:第三步修改代码后,第四步测试失败。Agent 回到第三步重新修改,但每次修改都引入了新的样式问题,测试始终不通过。循环了 7 次后 token 预算耗尽。
根因:没有为循环设置退出条件。状态机缺少「最大重试次数」和「升级条件」。
修复:
// ✅ 在状态机中加入退出条件和升级机制
interface StateMachineConfig {
maxRetriesPerStep: 3 // 单步最大重试次数
maxTotalSteps: 20 // 总步骤上限
escalationCondition: (ctx: ExecutionContext) => boolean
}
async function runWithBounds(config: StateMachineConfig, plan: AdaptivePlan) {
let totalSteps = 0
while (plan.currentIndex < plan.steps.length) {
if (totalSteps >= config.maxTotalSteps) {
// 超出总步骤上限,停下来而不是继续转圈
return { status: 'escalated', reason: '总步骤超限,需要人工介入' }
}
const result = await executeStep(plan.steps[plan.currentIndex])
if (result.failed) {
const stepRetries = getRetryCount(plan.currentIndex)
if (stepRetries >= config.maxRetriesPerStep) {
// 同一步骤重试 3 次仍失败,说明方向可能有根本问题
return { status: 'escalated', reason: `步骤 ${plan.currentIndex} 重试耗尽` }
}
}
totalSteps++
plan.currentIndex++
}
return { status: 'completed' }
}案例二:工具调用的权限泄漏
场景:这个 Agent 注册了 read_file、write_file、delete_file、run_command、create_pr 五个工具。一个实习生在使用时,Agent 在分析一个废弃文件时调用了 delete_file。
翻车:Agent 认为该文件「不再被引用」就自行删除了它。实际上该文件被一个 CI 脚本通过相对路径引用,删除导致 CI 流水线失败。
根因:高风险工具(删除文件、执行命令、创建 PR)没有权限分级,Agent 可以无限制调用。
修复:
// ✅ 工具按风险分级,高风险操作需要用户确认
interface ToolPermission {
riskLevel: 'low' | 'medium' | 'high' | 'critical'
autoApprove: boolean
requireConfirmation?: boolean
}
const toolPermissions: Record<string, ToolPermission> = {
read_file: { riskLevel: 'low', autoApprove: true },
write_file: { riskLevel: 'medium', autoApprove: true },
create_pr: { riskLevel: 'medium', autoApprove: false, requireConfirmation: true },
run_command: { riskLevel: 'high', autoApprove: false, requireConfirmation: true },
delete_file: { riskLevel: 'critical', autoApprove: false, requireConfirmation: true },
}
// 在工具调用前拦截
async function guardedToolCall(call: ToolCall, user: UserInfo) {
const perm = toolPermissions[call.name]
if (!perm.autoApprove && perm.requireConfirmation) {
const confirmed = await requestUserConfirmation({
tool: call.name,
arguments: call.arguments,
riskLevel: perm.riskLevel,
message: `Agent 想要调用 ${call.name},是否允许?`,
})
if (!confirmed) {
return { status: 'denied', reason: '用户拒绝' }
}
}
return await executeTool(call)
}| 风险等级 | 典型工具 | 自动执行 | 需要确认 |
|---|---|---|---|
| low | read_file, search | ✅ | ❌ |
| medium | write_file, create_pr | ❌ | ✅ |
| high | run_command, deploy | ❌ | ✅(二次确认) |
| critical | delete_file, drop_table | ❌ | ✅(强制拦截) |
案例三:上下文膨胀导致推理质量下降
场景:一个复杂的 Issue 涉及 5 个文件、3 次代码搜索、2 次测试运行。Agent 到第 15 步时,上下文已经积累了 50K token。
翻车:Agent 在第 16 步「总结修改方案」时,给出了一个与第 3 步分析结果完全矛盾的结论。它「忘记」了之前发现的关键约束。
根因:上下文过长导致注意力衰减,关键信息被淹没在大量中间过程中。
修复:引入「阶段性总结」机制。每完成一个计划阶段,用 LLM 将阶段性结果压缩为摘要,替换掉详细的过程记录。
// ✅ 阶段性总结:用摘要替换过程详情
async function compactPhaseMemory(
phaseName: string,
messages: Message[],
): Promise<Message[]> {
// 保留阶段结论,丢弃中间过程
const summary = await llm.generate({
system: '将以下对话阶段压缩为结构化摘要,保留关键发现和结论,丢弃中间搜索和试错过程。',
messages,
})
return [{
role: 'system',
content: `[阶段总结 - ${phaseName}]\n${summary}`,
}]
}
// 在主循环中阶段性调用
async function runWithCompaction(plan: AdaptivePlan) {
let phaseMessages: Message[] = []
for (const step of plan.steps) {
const result = await executeStep(step)
phaseMessages.push(step.message, result.message)
// 每个阶段结束时压缩
if (step.isPhaseEnd) {
const compacted = await compactPhaseMemory(step.phaseName, phaseMessages)
memory.replace(phaseMessages, compacted) // 用摘要替换原始消息
phaseMessages = compacted
}
}
}可观测性:让 Agent 的每一步都可回放
最后一个模块,但重要性不亚于前面任何一个。没有可观测性的 Agent 系统就是一个黑盒——你知道它输出了什么,但不知道它为什么这样输出。
Braintrust 在 2026 年的 Agent 可观测性指南中提出了 Agent trace 的四个基本支柱:4
| 支柱 | 记录内容 | 典型字段 |
|---|---|---|
| 工具调用 | 每次工具调用的完整生命周期 | name, arguments, output, latency, retries |
| 推理步骤 | 模型的思考、计划和决策过程 | thought, action, observation, decision |
| 状态转换 | 工作记忆的每一步变化 | state_before, state_after, trigger |
| 记忆操作 | 记忆的读写和检索 | operation, query, results, relevance_scores |
// ❌ 坏做法:用 console.log 散点记录
console.log('Calling tool:', toolName)
const result = await callTool(toolName, args)
console.log('Tool result:', result)
// ✅ 好做法:结构化 trace,每个事件有类型、时间和关联
interface AgentTraceEvent {
traceId: string // 同一次运行的所有事件共享
spanId: string // 当前事件唯一标识
parentSpanId?: string // 父事件(支持嵌套)
type: 'tool_call' | 'reasoning' | 'state_transition' | 'memory_op'
timestamp: number
duration?: number
input?: unknown
output?: unknown
error?: { type: string; message: string }
metadata?: Record<string, unknown>
}
class AgentTracer {
private events: AgentTraceEvent[] = []
async traceToolCall(traceId: string, parentSpanId: string, call: ToolCall) {
const spanId = crypto.randomUUID()
const start = Date.now()
try {
const result = await executeTool(call)
this.events.push({
traceId, spanId, parentSpanId,
type: 'tool_call',
timestamp: start,
duration: Date.now() - start,
input: { tool: call.name, arguments: call.arguments },
output: result,
})
return result
} catch (error) {
this.events.push({
traceId, spanId, parentSpanId,
type: 'tool_call',
timestamp: start,
duration: Date.now() - start,
input: { tool: call.name, arguments: call.arguments },
error: { type: error.name, message: error.message },
})
throw error
}
}
// 导出完整 trace 用于回放
exportTrace(traceId: string): AgentTraceEvent[] {
return this.events
.filter(e => e.traceId === traceId)
.sort((a, b) => a.timestamp - b.timestamp)
}
}Google Cloud 的架构指南建议将 OpenTelemetry 作为 Agent 可观测性的基础协议。1 OpenTelemetry 2026 年已经开始支持 MCP 协议的 span 标准化,这意味着不同框架(LangGraph、CrewAI、Vercel AI SDK)构建的 Agent 可以用统一的 trace 格式记录和分析。5
多 Agent 系统还有一个特殊的挑战:Agent 之间的上下文传递。Braintrust 的建议是使用 parent-child span 跨 Agent 边界传播——将 Agent A 传递给 Agent B 的上下文记录为 parent agent trace 中的一个 span,Agent B 的运行嵌套在这个 span 之下,同一个 traceId 贯穿两个 Agent。4 这样在排查问题时,可以在一个视图中看到完整的调用链。
生产环境检查清单
以下清单来自我在实际项目中总结的经验,按实施阶段分组。每一项都可以通过具体的技术手段验证。
设计阶段
- 明确六个模块的职责边界,画出模块间的依赖关系图
- 为每个工具定义 JSON Schema,包括参数类型、必填项、取值范围
- 确定计划模式(ReAct / Plan-and-Execute / 混合),并写明选择理由
- 定义状态机的所有合法状态和转换条件,包括终止条件
- 为工具按风险分级(low / medium / high / critical),确定哪些需要用户确认
实现阶段
- 工具调用前做参数校验(schema 校验 + 业务规则校验),不信任模型生成的参数
- 记忆模块实现分层压缩策略,设定工作记忆上限和压缩触发条件
- 状态机实现总步骤上限和单步重试上限,防止死循环
- 权限层独立于工具层实现,作为拦截器模式存在
- 所有工具调用、推理步骤、状态转换、记忆操作都写入结构化 trace
测试阶段
- 用「异常输入」测试计划模块:给一个不可能完成的目标,验证 Agent 是否能正确退出
- 用「越权请求」测试权限层:让 Agent 调用超出角色权限的工具,验证是否被拦截
- 用「长对话」测试记忆模块:超过 50 轮对话后,验证关键信息是否仍可被检索
- 用「工具故障」测试容错:模拟工具返回错误或超时,验证 Agent 不会编造成结果
运维阶段
- 部署 trace 采集和查询系统(推荐 Langfuse、Braintrust 或基于 OpenTelemetry 自建)
- 设定 token 消耗预算和告警阈值
- 建立线上评估(online evaluation)机制,对生产 trace 做自动质量评分
- 定期将线上失败案例转化为测试用例,形成回归防护
写在最后
Agent 架构不是一个新领域,它是软件工程的经典问题在新场景下的应用。计划模块对应任务调度,工具模块对应 API 网关,记忆模块对应缓存策略,权限模块对应访问控制,状态机对应工作流引擎,可观测性对应 APM。
区别在于:传统软件系统的行为是确定性的,你可以写测试覆盖所有分支;Agent 系统的行为是非确定性的,同样的输入可能走出完全不同的路径。这意味着我们需要从「控制行为」转向「约束行为」——通过架构边界、权限控制、退出条件和可观测性,让 Agent 在安全的范围内自主决策。
我在这一年学到的最重要的一课是:不要试图让一个 Prompt 解决所有问题。就像微服务架构把一个单体应用拆成多个独立服务一样,Agent 架构把一个长 Prompt 拆成多个独立模块。拆分之后,每个模块可以独立优化、独立测试、独立替换。这才是架构的价值。
参考资料
Footnotes
-
Google Cloud, "Choose a Design Pattern for Your Agentic AI System", 2026. https://docs.cloud.google.com/architecture/choose-design-pattern-agentic-ai-system ↩ ↩2
-
Manus, "Context Engineering for AI Agents: Lessons from Building Manus", 2025. https://manus.im/zh-cn/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus ↩ ↩2 ↩3
-
LangChain, "State of Agent Engineering", 2026. https://www.langchain.com/state-of-agent-engineering ↩
-
Braintrust, "Agent Observability: The Complete Guide for 2026". https://www.braintrust.dev/articles/agent-observability-complete-guide-2026 ↩ ↩2
-
MintMCP, "OpenTelemetry for AI Agents: Implementing Observability in MCP", 2026. https://www.mintmcp.com/blog/opentelemetry-ai-agents ↩