AI Agent 的软件架构:计划、工具、记忆和权限

0阅读13分钟

Agent 不是一个大 Prompt

去年我在一个内部工具项目里,把整个 Agent 的能力写在一个 3000 行的 System Prompt 里——计划、工具调用、记忆管理、权限判断全混在一起。上线第一周,它在 80% 的简单任务上表现惊人;第二周,用户开始报告它在复杂任务里「忘记」之前的步骤、调用不该调的工具、甚至编造执行结果。

问题的根源不是模型能力不够,而是架构上的混乱:所有职责耦合在一起,没有清晰的模块边界,出了问题无法定位,也无法单独替换某个模块。这和我过去做分布式系统时的经验完全一致——当一个服务承担了太多职责,它一定会在某些边界条件下失控。

Agent 架构本质上是一个软件工程问题。LLM 提供了推理能力,但围绕它的计划、工具、记忆、权限、状态和监控模块,需要用经典的软件工程方法来设计。Google Cloud 在 2026 年发布的 Agentic AI 架构指南中明确建议:「如果你刚开始 Agent 开发,从单 Agent 开始,聚焦核心逻辑,再逐步增加复杂度。」1 这句话的重点不在「单 Agent」,而在「逐步增加复杂度」——每一步增加都应该是架构上可控的。

这篇文章来自我过去一年在生产环境中设计和调试 Agent 系统的实际经验。我会用具体的案例、代码和对比表格,拆解 Agent 架构中最关键的六个模块,以及它们之间的协作方式。

核心架构:六个模块的职责边界

一个生产级 Agent 系统可以拆成六个独立模块,每个模块有明确的输入输出和职责边界:

模块核心职责输入输出典型失败模式
计划模块将用户目标分解为可执行步骤用户意图、当前状态步骤列表、执行顺序计划漂移、死循环
工具模块执行外部操作并返回结果工具名称、参数执行结果、错误信息参数幻觉、静默失败
记忆模块管理短期和长期上下文对话历史、中间状态压缩后的上下文上下文丢失、记忆泄漏
权限模块控制工具访问和操作边界工具调用请求、用户角色允许/拒绝/需确认越权操作、过度拦截
状态机管理执行流程和状态转换当前步骤、执行结果下一步状态、终止条件状态跳跃、无法终止
可观测性记录全过程并支持回放所有模块的执行事件结构化 trace、指标黑盒运行、无法复盘

这些模块之间不应存在隐式依赖。计划模块不应该知道工具的具体实现细节,工具模块不应该关心记忆如何压缩上下文,权限模块应该作为独立的拦截层存在。这种解耦不只是为了代码整洁——它让每个模块可以独立测试、独立替换、独立升级。

流程图画布 · 115%
Mermaid 流程图加载中...

这个流程图展示了一个完整的 Agent 执行循环。关键设计决策在于:权限检查在工具执行之前,状态转换在结果回来之后,可观测性作为旁路记录所有事件而不是嵌入业务逻辑。

计划模块:ReAct 和 Plan-and-Execute 的工程取舍

计划模块是 Agent 的「大脑」。目前主流的两种模式是 ReAct(Reasoning and Acting)和 Plan-and-Execute。

ReAct 模式让模型在每一步先推理再行动,观察结果后决定下一步。它适合任务路径不确定的场景——比如一个需要多步搜索的研究助手。Plan-and-Execute 模式让模型先列出完整计划,再逐步执行,适合步骤可预见的结构化任务——比如数据处理流水线。

这两种模式的选择不是一个理论问题,而是一个工程权衡:

维度ReActPlan-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 里写一句「请合理使用工具」。这在原型阶段可以工作,在生产环境中会制造三种问题:

  1. 参数幻觉:模型编造不存在的参数值,比如填一个虚构的文件路径
  2. 越权调用:模型调用了超出当前用户权限的工具
  3. 静默失败:工具返回错误,模型在下一轮推理中假装调用成功

这三种问题的根源相同:工具调用缺乏结构化的约束和验证。

维度无约束工具调用结构化约束
工具注册全量注册,Agent 自由选择按角色和场景动态注册
参数校验依赖模型遵循 schemaschema 校验 + 业务规则校验
权限控制无,或仅在 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),把所有信息都塞进去就行了。实际运行中,上下文过长会导致三个问题:

  1. 注意力衰减:模型对上下文中间部分的关注度明显低于首尾(「Lost in the Middle」现象)
  2. 推理质量下降:无关信息越多,模型的推理准确率越低
  3. 成本和延迟线性增长:每个 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_filewrite_filedelete_filerun_commandcreate_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)
}
风险等级典型工具自动执行需要确认
lowread_file, search
mediumwrite_file, create_pr
highrun_command, deploy✅(二次确认)
criticaldelete_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

  1. 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

  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

  3. LangChain, "State of Agent Engineering", 2026. https://www.langchain.com/state-of-agent-engineering

  4. Braintrust, "Agent Observability: The Complete Guide for 2026". https://www.braintrust.dev/articles/agent-observability-complete-guide-2026 2

  5. MintMCP, "OpenTelemetry for AI Agents: Implementing Observability in MCP", 2026. https://www.mintmcp.com/blog/opentelemetry-ai-agents

评论 0

0 / 1000