ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent 五层架构实战:从接入感知到治理观测的工程落地指南

Agent 五层架构实战:从接入感知到治理观测的工程落地指南 1. 为什么我要把 Agent 拆成五层来看1.1 从“能跑”到“能交付”的鸿沟过去一年我经手过不下十个 Agent 项目从最简单的“调个 API 回答问题”到带工具调用、带记忆、带多步规划的系统都有。踩坑最多的阶段不是写不出能跑的 demo而是 demo 跑通之后一旦换一批输入、换一个用户、换一个任务类型整个东西就开始胡言乱语、乱调工具、无限循环。后来我意识到问题不在于某个 prompt 写得不好而在于我一开始就没有把 Agent 当成一个有层次结构的系统来设计而是把它当成“一个大模型加几个 if-else”。这篇内容我想干的事很明确把我在实际项目里反复验证过的一套 Agent 五层能力架构完整拆开讲。这五层分别是接入与感知层、认知与规划层、记忆与知识层、工具与执行层、治理与观测层。它不是一个学术框架而是从工程落地角度倒推出来的分层方式每一层都对应着具体的代码模块、具体的失败模式和具体的排查手段。如果你正在做 Agent 开发不管是基于 LangGraph 这种图编排框架还是自己手写 ReAct 循环或者是接 MCP 工具生态这套分层都能直接套用。新手可以把它当成搭建路线图有经验的人可以拿它当 checklist 去审视自己现有系统缺了哪一块。我下面会结合 ReAct、RAG、MCP、LangGraph 这些热词背后的真实工程含义一层一层讲清楚“为什么这么分”以及“每一层具体怎么落地”。1.2 五层架构的整体轮廓先把全景摆出来后面再逐层展开。我用一个表格把五层的职责、典型组件和最常见的翻车点列出来你可以先对照自己的项目看看哪一层是空的。层级核心职责典型组件常见翻车点接入与感知层接收输入、解析意图、标准化上下文输入解析器、多模态适配、会话管理输入格式不统一导致下游全乱认知与规划层任务分解、推理决策、行动选择ReAct 循环、规划器、路由无限循环、规划粒度失控记忆与知识层短期上下文、长期记忆、外部知识RAG、向量库、结构化知识库检索噪音大、记忆污染工具与执行层调用外部能力、执行动作MCP、函数调用、沙箱工具描述不清、参数幻觉治理与观测层安全、成本、可观测、可回滚日志、追踪、限流、审计出问题无法定位、成本失控这五层不是严格串行的流水线而是相互嵌套的。规划层会反复调用记忆层和工具层治理层则像一张网罩在所有层上面。理解这一点很关键否则你会把它当成瀑布模型去设计结果发现根本跑不通。2. 接入与感知层别让脏输入毁掉整个系统2.1 这一层到底在解决什么问题很多人觉得接入层就是“收个字符串传给模型”没什么技术含量。我一开始也这么想直到有一次线上事故用户从不同渠道进来的输入里混了各种不可见字符、超长文本、嵌套的 JSON 字符串模型直接开始输出乱码工具调用参数全是错的。那次之后我才明白接入层的核心任务是把千奇百怪的外部输入归一化成下游可以稳定处理的内部表示。具体来说这一层要做四件事第一解析输入的结构区分是纯文本、结构化数据还是多模态内容第二做初步的意图识别判断这是闲聊、任务请求还是工具触发第三管理会话状态把多轮对话的上下文正确拼接第四做输入清洗去掉控制字符、限制长度、处理编码问题。这四件事任何一件没做好下游的规划层就会收到“有毒”的输入再聪明的推理也救不回来。2.2 输入归一化的实操要点我现在的做法是定义一个统一的内部消息结构所有外部输入都必须先转成这个结构才能进入后续流程。这个结构至少包含角色、内容类型、原始内容、清洗后内容、时间戳、会话 ID、来源渠道。用 Python 的 dataclass 或者 Pydantic 模型来定义好处是类型明确、校验方便。from pydantic import BaseModel, Field from typing import Literal, Optional from datetime import datetime class NormalizedMessage(BaseModel): role: Literal[user, assistant, system, tool] content_type: Literal[text, json, image_ref, file_ref] raw_content: str clean_content: str session_id: str source: str timestamp: datetime Field(default_factorydatetime.now)清洗逻辑我一般分三步走。第一步是字符级清洗用正则去掉控制字符和零宽字符这类字符肉眼看不见但会干扰模型的分词。第二步是长度控制超长输入不能直接截断因为截断可能把关键信息切掉我的做法是先按语义段落切分再按重要性排序保留或者用一个小模型做摘要压缩。第三步是编码统一全部转成 UTF-8避免出现乱码。注意长度控制千万不要简单粗暴地按字符数截断。我踩过的坑是用户把关键参数放在最后一截断参数就没了模型只能瞎猜。宁可多花点 token 做摘要也不要丢信息。2.3 意图初筛为什么值得单独做意图初筛的价值在于省钱和省时间。不是每个输入都需要走完整的规划流程。一句“你好”或者“谢谢”完全没必要触发工具调用和 RAG 检索。我在接入层后面加了一个轻量级的分类器可以是规则匹配也可以是一个小模型把输入分成几类闲聊类直接走快速回复通道任务类进入完整流程工具触发类直接路由到对应工具。这个分类器不需要很准它的作用是过滤掉明显不需要复杂处理的输入。实测下来这一层能挡掉大概三成的无效请求直接降低了后续的 token 消耗和延迟。分类的阈值可以调宁可漏放一些到完整流程也不要把真正的任务请求误判成闲聊。3. 认知与规划层ReAct 不是万能药3.1 ReAct 循环的本质与边界ReAct 这个词现在被用得很泛好像只要让模型“思考一步、行动一步”就叫 ReAct。但它的本质其实是一个交替进行的推理-行动循环模型先输出一段推理Thought然后决定一个行动Action执行后拿到观察结果Observation再基于观察继续推理直到得出最终答案。这个模式在 LangGraph 里被抽象成了节点和边的图结构每个节点是一个推理或行动步骤边决定下一步走向。ReAct 好用的场景是步骤数不多、每步依赖上一步结果的任务比如“查天气然后决定穿什么”。但它有明显的边界。第一步骤一多就容易跑偏因为每一步的推理都基于前面的上下文误差会累积。第二它不擅长需要全局规划的任务比如“帮我规划一个五天的行程”这种任务更适合先规划再执行而不是边走边看。第三它对工具描述的依赖极强工具描述写得含糊模型就会乱调。我的经验是把 ReAct 当成规划层的一种模式而不是唯一模式。简单任务用 ReAct复杂任务用“先规划后执行”的两阶段模式需要多轮交互的用状态机模式。LangGraph 的好处就是这几种模式可以混用用图结构把它们串起来。3.2 规划粒度失控怎么破规划层最常见的翻车是粒度失控。要么规划得太粗模型一步就想干完所有事结果工具调用参数一大堆全是幻觉要么规划得太细把一个简单任务拆成二十步每步都要调一次模型成本和延迟爆炸。我的做法是给规划设一个步数预算和粒度约束。步数预算就是硬性限制最多几步超了就强制收敛。粒度约束是在 prompt 里明确告诉模型每一步应该是一个“可独立执行、有明确输入输出”的动作。比如“查询北京今天的天气”是一个合适的粒度“处理用户的所有需求”就太粗“打开浏览器”就太细。还有一个技巧是用结构化输出来约束规划。让模型输出一个 JSON 格式的计划每个步骤包含动作类型、参数、预期结果。这样既方便程序解析也逼着模型把计划想清楚。LangGraph 里可以用 Pydantic 模型做输出校验格式不对就重试。from pydantic import BaseModel from typing import List, Literal class PlanStep(BaseModel): step_id: int action_type: Literal[tool_call, reasoning, retrieval, respond] description: str tool_name: Optional[str] None parameters: Optional[dict] None class Plan(BaseModel): steps: List[PlanStep] max_steps: int 83.3 路由与分支的设计心得规划层还有一个容易被忽视的能力是路由。不是所有请求都走同一条路径。我在实际项目里会设几个路由分支需要外部知识的走 RAG 分支需要执行动作的走工具分支纯推理的走直接回答分支需要多轮澄清的走交互分支。路由的判断可以基于意图分类也可以让模型自己选但后者不稳定我倾向于用规则加小模型结合的方式。LangGraph 的 conditional edge 就是干这个的。你可以在图里定义一个判断节点根据当前状态决定走哪条边。这个设计比在一个大 prompt 里让模型自己决定要灵活得多也更容易调试因为每条路径是独立的出问题能快速定位是哪条分支的逻辑错了。4. 记忆与知识层RAG 的瓶颈到底在哪4.1 短期记忆与长期记忆的分工记忆层经常被和 RAG 混为一谈其实它们解决的是不同问题。短期记忆是当前会话的上下文解决的是“刚才说了什么”长期记忆是跨会话的持久化信息解决的是“这个用户以前说过什么”外部知识是 RAG 检索的内容解决的是“我不知道但知识库里有”。这三者要分开管理混在一起就会互相污染。短期记忆的管理核心是上下文窗口的取舍。对话一长不可能全塞进去。我的做法是保留最近 N 轮完整对话更早的做摘要压缩关键信息比如用户明确说的偏好、约束单独抽出来存成结构化字段。这样既控制了 token又不丢关键信息。长期记忆我一般用向量库加结构化存储的组合。向量库存语义化的记忆片段结构化库存明确的字段比如用户 ID、偏好标签、历史任务类型。检索的时候两者结合先用结构化字段过滤再用向量做语义匹配。这样比纯向量检索准得多。4.2 RAG 瓶颈的真实来源RAG 被吐槽最多的是“检索出来的东西没用”。但瓶颈往往不在检索算法本身而在切分策略和查询构造。我见过太多项目把文档按固定字数切分结果一个完整的逻辑被切成两半检索出来半截话模型只能瞎编。正确的做法是按语义结构切分比如按标题层级、按段落、按代码块保证每个 chunk 是一个自包含的语义单元。查询构造也是大坑。用户问“这个怎么配置”直接拿这句话去检索命中率极低因为文档里不会写“这个怎么配置”。我的做法是先让模型把用户问题改写成几个具体的检索查询每个查询针对一个可能的答案方向然后并行检索再合并结果。这一步叫查询扩展实测能把召回率提升一大截。瓶颈类型表现解决方向切分不当检索结果语义不完整按语义结构切分保留上下文查询模糊召回率低查询扩展、多查询并行噪音过多检索结果相关性差重排序、阈值过滤知识过时答案与现状不符增量更新、时效性标注4.3 结构化知识库与向量库的配合热词里提到“KG 知识库、RAG 知识库和结构知识库的区分”这个问题很实际。向量库擅长模糊语义匹配但不擅长精确的关系查询。比如“A 和 B 是什么关系”这种问题向量库检索出来的是一堆相关文本而结构化知识库比如图数据库能直接给出关系路径。我的做法是两者配合。先用结构化知识库处理明确的关系查询和过滤条件把范围缩小再用向量库在缩小后的范围里做语义匹配。比如用户问“我们产品里支持 MCP 的工具都有哪些”先用结构化查询过滤出“支持 MCP”这个标签的工具列表再用向量检索匹配用户具体想找的功能。这样既准又快。至于“RAG 知识库能不能存图片”答案是能但要用多模态 embedding。图片先转成向量存进去检索时用文本查询匹配图片向量。不过实测下来纯图片检索的效果不如“图片加文字描述”的组合所以我在存图片的时候会同时存一段自动生成的描述文本检索走文本返回时带上图片引用。5. 工具与执行层MCP 到底解决了什么5.1 从函数调用到 MCP 的演进逻辑早期做工具调用就是写一堆函数把函数签名和描述塞进 prompt让模型输出函数名和参数。这种方式的问题很明显工具一多prompt 就爆炸工具描述格式不统一模型经常调错新增工具要改代码重新部署。MCP 出现的意义就是把工具的提供和使用解耦。MCP 本质上是一套标准协议规定了工具怎么描述自己、怎么被调用、怎么返回结果。工具提供方实现 MCP 服务端Agent 作为客户端去连接双方通过标准协议通信。这样工具可以独立部署、独立升级Agent 不需要知道工具的内部实现只需要知道它暴露的能力。这跟微服务的思路是一样的只是把服务换成了“模型可调用的能力”。我实测下来MCP 最大的价值不是技术上的而是生态上的。以前每个 Agent 项目都要自己写一遍工具集成现在只要工具支持 MCP任何 Agent 都能直接用。这对 Agent 开发的效率提升是数量级的。5.2 工具描述怎么写才不被模型误用工具调用失败十有八九是描述没写好。模型对工具的理解完全来自你给的描述描述含糊模型就瞎猜。我总结了几条写工具描述的硬规则。第一名称要动词开头、语义明确。search_documents比doc_tool好send_email比email好。第二描述要说清楚什么时候用、什么时候不用。只写“搜索文档”不够要写“当用户询问已有文档中的信息时使用不要用于实时数据查询”。第三参数要标注类型、是否必填、取值范围。模型对枚举值特别敏感给了枚举它就不会乱填。第四给出调用示例。一个具体的输入输出示例比一大段描述都管用。{ name: search_knowledge_base, description: 在内部知识库中搜索文档。适用于用户询问产品文档、操作指南类问题。不适用于实时数据或外部信息查询。, parameters: { query: { type: string, description: 搜索关键词应该是具体的名词短语不要用完整句子, required: true }, top_k: { type: integer, description: 返回结果数量, enum: [3, 5, 10], default: 5 } } }注意工具描述里的“不要用于”比“用于”更重要。模型很容易过度使用某个工具明确划出边界能大幅减少误调用。5.3 执行沙箱与失败处理工具执行一定要有沙箱和超时。我踩过的坑是某个工具调用卡死整个 Agent 流程就挂在那里用户等半天没反应。现在我的做法是每个工具调用都设超时超时后返回一个明确的错误信息给模型让模型决定是重试还是换方案。失败处理的关键是把错误信息结构化地返回给模型。不要只返回“调用失败”要返回失败类型、失败原因、可能的解决方向。模型拿到这些信息才能做出合理的下一步决策。比如“参数格式错误query 字段应该是字符串实际收到的是数组”模型看到这个就知道要改参数格式。还有一个技巧是幂等性设计。有些工具调用是有副作用的比如发邮件、写数据库。这类工具要支持幂等同一个请求重复调用不会产生重复副作用。否则模型重试的时候就会出问题。6. 治理与观测层看不见的第五层6.1 为什么治理层必须从第一天就建治理层是最容易被忽略的一层因为它在 demo 阶段完全看不出价值。但一旦上线没有治理层的 Agent 就是个黑盒出了问题你连从哪查都不知道。我现在的原则是治理层从第一天就要建哪怕最开始只是简单的日志。治理层要管四件事成本、安全、可观测、可回滚。成本是 token 消耗和工具调用次数要有预算和限流。安全是输入输出过滤、权限控制、敏感操作审计。可观测是全链路追踪每一步的输入输出、耗时、结果都要记录。可回滚是当新版本出问题时能快速切回旧版本。这四件事里可观测是基础。没有可观测其他三件都无从谈起。我一般用 OpenTelemetry 这类标准做追踪每个 Agent 流程生成一个 trace每个步骤是一个 span记录输入输出和耗时。这样出问题能精确定位到是哪一步、哪个工具、哪个 prompt 出的错。6.2 成本失控的常见原因与对策Agent 的成本失控通常来自三个地方。第一是无限循环模型在两个状态之间反复横跳每跳一次就烧一次 token。对策是设最大步数硬限制超了强制终止。第二是上下文膨胀每轮都把全部历史塞进去token 随轮数线性增长。对策是上下文压缩和摘要。第三是工具调用过多模型把简单任务拆成一堆工具调用。对策是优化工具描述和规划粒度。我一般会给每个会话设一个 token 预算快超的时候触发降级策略比如切换到更小的模型、减少检索结果数量、简化 prompt。这个预算可以根据用户等级或者任务类型动态调整。成本问题根因对策无限循环缺少终止条件最大步数限制、循环检测上下文膨胀全量历史拼接摘要压缩、关键信息抽取工具滥用描述不清、规划过细优化描述、粒度约束模型过大未分级使用按任务复杂度选模型6.3 安全边界与审计日志Agent 的安全问题比传统应用更复杂因为它会自主决策、自主调用工具。我关注的安全点有三个输入注入、越权操作、输出泄露。输入注入是用户在输入里藏指令试图让 Agent 执行非预期操作。对策是在接入层做输入清洗和指令隔离把用户输入和系统指令明确分开。越权操作是 Agent 调用了不该调用的工具或访问了不该访问的数据。对策是工具级权限控制每个工具调用前校验权限。输出泄露是 Agent 把敏感信息输出给了不该看到的用户。对策是输出过滤和脱敏。审计日志要记录所有关键决策点模型选了什么工具、传了什么参数、拿到了什么结果、最终输出了什么。这些日志不仅是排查问题的依据也是合规审计的凭证。我一般会把日志存成结构化格式方便后续查询和分析。7. 五层怎么串起来一个完整的落地路径7.1 从最小可用到完整架构的演进不要一上来就把五层全建齐那样太重了。我的建议是分阶段演进。第一阶段只建接入层和认知层做一个能对话、能简单推理的 Agent验证核心流程。第二阶段加工具层接入几个关键工具验证工具调用链路。第三阶段加记忆层接入 RAG验证知识检索。第四阶段加治理层把日志、成本、安全补上。第五阶段回头优化每一层做精细化的调优。这个顺序的逻辑是先跑通再优化。每一阶段都是一个可用的系统而不是半成品。这样你能快速拿到反馈也能在任何一个阶段停下来不至于投入大量精力却看不到成果。7.2 用 LangGraph 串起五层的实操示例LangGraph 的好处是它天然适合表达这种分层结构。你可以把每一层做成一个子图然后用主图把它们串起来。下面是一个简化的骨架展示五层怎么在代码里对应起来。from langgraph.graph import StateGraph, END from typing import TypedDict, List class AgentState(TypedDict): messages: List[dict] plan: List[dict] retrieved_docs: List[str] tool_results: List[dict] step_count: int trace_id: str def build_agent_graph(): graph StateGraph(AgentState) # 接入层输入归一化 graph.add_node(ingest, ingest_node) # 认知层规划 graph.add_node(plan, plan_node) # 记忆层检索 graph.add_node(retrieve, retrieve_node) # 工具层执行 graph.add_node(execute, execute_node) # 治理层记录与校验 graph.add_node(govern, govern_node) graph.set_entry_point(ingest) graph.add_edge(ingest, plan) graph.add_conditional_edges(plan, route_after_plan, { retrieve: retrieve, execute: execute, respond: END }) graph.add_edge(retrieve, plan) graph.add_edge(execute, govern) graph.add_edge(govern, plan) return graph.compile()这个骨架里plan节点是核心它根据当前状态决定下一步走检索、执行还是直接回复。govern节点负责记录和校验每次工具执行后都过一遍。step_count用来做步数限制超了就强制走 END。7.3 每一层的验收标准怎么判断一层建好了我给自己定了几个验收标准。接入层各种格式的输入都能正确归一化脏输入不会导致下游崩溃。认知层简单任务能正确规划复杂任务不会无限循环步数在预算内。记忆层检索结果相关性达标不会检索出明显无关的内容。工具层工具调用成功率达标参数错误率低失败能正确恢复。治理层全链路可追踪成本在预算内安全过滤有效。这些标准不是一次性能达到的而是持续优化的目标。我一般会建一个评测集每次改动后跑一遍看各项指标有没有退化。这个评测集不用很大几十个典型场景就够关键是覆盖各种边界情况。8. 常见问题速查与避坑清单8.1 高频问题排查表问题现象可能原因排查方向模型乱调工具工具描述不清检查描述是否明确边界和参数无限循环缺少终止条件检查步数限制和循环检测检索结果无关切分或查询问题检查 chunk 质量和查询扩展上下文丢失压缩过度检查摘要是否保留关键信息成本飙升模型过大或调用过多检查模型分级和工具调用次数响应慢串行调用过多检查能否并行化输出不稳定温度过高或 prompt 模糊检查温度和 prompt 明确性8.2 我踩过的几个典型坑第一个坑是过早优化。一开始就想着把五层都建得很完美结果花了两周还没跑通一个完整流程。后来改成先跑通再优化两天就出了可用版本。这个教训是Agent 开发要快速迭代不要追求一次到位。第二个坑是忽视工具描述。有次工具调用成功率只有六成查了半天以为是模型问题最后发现是工具描述写得太含糊模型根本不知道什么时候该用。改完描述后成功率直接到九成五。这个教训是工具描述值得花时间打磨。第三个坑是没有成本监控。上线第一周账单超预期三倍查下来是某个场景触发了无限循环。加了步数限制和成本告警后就正常了。这个教训是治理层要从第一天就建哪怕只是最简单的监控。第四个坑是RAG 切分太随意。按固定字数切分导致检索出来的内容都是半截话模型只能瞎编。改成按语义结构切分后答案质量明显提升。这个教训是RAG 的效果很大程度上取决于数据准备而不是检索算法。8.3 给不同阶段开发者的建议如果你是刚接触 Agent 开发我的建议是先用现成框架跑通一个最小 demo理解 ReAct 循环和工具调用的基本流程然后再考虑分层。不要一上来就自己造轮子LangGraph 这类框架已经帮你处理了很多底层细节。如果你已经有一个能跑的 Agent但效果不稳定我的建议是拿这五层当 checklist 逐层审视。大概率你会发现某一层是空的或者某一层的实现有问题。补上那一层效果往往会有明显提升。如果你在做生产级的 Agent 系统我的建议是把治理层当成一等公民。成本、安全、可观测这三件事在 demo 阶段看不出价值但在生产环境里决定了系统能不能活下去。我见过太多技术很牛但因为没有治理层而上不了线的项目。最后分享一个我自己的习惯每做一个新 Agent我都会先画一张五层图标出每一层用什么方案、有哪些已知风险、验收标准是什么。这张图不一定给别人看但它能帮我在开发过程中不迷失方向。Agent 开发很容易陷入细节有一个全局视角能让你在遇到问题时快速判断是哪一层的问题而不是盲目地改 prompt。
返回列表