Agent 架构设计:三层上下文裁剪模式详解
本文分享了在项目管理系统的 AI 模块开发中,针对 Agent 架构上下文管理的演进过程。从最初的 ReAct 模式到 Plan-Execute,再到 Router 分发,最终提出三层上下文裁剪模式。通过 Context Assembler 将完整页面上下文拆分为路由、执行、模型三个视图,分别控制信息流动。该方案解决了 Token 浪费、安全风险及职责耦合问题,确立了最小权限原则,确保每个环节只获取必要信息,提升了系统的可控性与成本效益。

本文分享了在项目管理系统的 AI 模块开发中,针对 Agent 架构上下文管理的演进过程。从最初的 ReAct 模式到 Plan-Execute,再到 Router 分发,最终提出三层上下文裁剪模式。通过 Context Assembler 将完整页面上下文拆分为路由、执行、模型三个视图,分别控制信息流动。该方案解决了 Token 浪费、安全风险及职责耦合问题,确立了最小权限原则,确保每个环节只获取必要信息,提升了系统的可控性与成本效益。

我最近在做一个项目管理系统的 AI 模块——不是独立的聊天窗口,而是嵌入到业务页面里的智能助手。用户在项目详情页、任务列表、首页工作台等不同页面,都可以直接跟 AI 交互:拆解任务、生成周报、分析延期风险。
听起来不复杂,但做到后面发现,Agent 架构中最容易被忽视、却最容易出问题的环节,不是模型选型,不是 prompt 调优,而是上下文的流动方式——哪些信息该给谁看,哪些信息不该给谁看。
这篇文章会先介绍一些 Agent 的基础概念,从最初的 ReAct 模式,到 Plan-Execute,再到 Router 分发,最终演化出"三层上下文裁剪"模式的思考。
如果你已经熟悉 Agent、ReAct、LangGraph 这些概念,可以直接跳到第二节。
在 AI 语境下,Agent 不是简单的"聊天机器人"。聊天机器人只做一件事:接收文本,返回文本。而 Agent 可以自主决策并执行动作。
一个典型的 Agent 工作流程是:
用户输入 → 理解意图 → 决定做什么 → 调用工具执行 → 观察结果 → 决定下一步 → ... → 返回最终回答
中间的"决定做什么"和"调用工具"是关键区别。Agent 可以查数据库、调 API、创建任务、发送通知——它不只是生成文字,而是真的在做事。
这也意味着 Agent 比聊天机器人危险得多:聊天机器人出错只是说错话(文本风险),Agent 出错是做错事(操作风险)——它可能删掉你的任务、修改错误的数据。
Tool 是 Agent 能调用的具体能力。你可以理解为 Agent 的"手脚":
query_tasks:查询任务列表create_task:创建新任务update_task:修改任务状态delete_task:删除任务Agent 本身是大脑(LLM),Tool 是它能操作外部世界的接口。Agent 根据用户意图,决定调用哪个 Tool、传什么参数。
LangGraph 是 LangChain 团队开发的一个框架,用于构建基于图(Graph)的 Agent 系统。它的核心思想是把 Agent 的工作流建模为一个状态机:
相比直接用 LLM 做链式调用,LangGraph 的优势在于:支持循环(Agent 可以反复思考)、支持中断和恢复(需要人工审批时可以暂停)、状态可持久化(断线重连不丢失进度)。
Agent 做决策时需要知道"当前状况"。在我的场景里,上下文就是用户当前的页面状态:
这些信息决定了 Agent 应该怎么理解用户的意图、怎么执行操作。问题在于:不同的处理环节需要的信息量是完全不同的。
我最初的方案是业内最经典的 ReAct(Reasoning + Acting)模式。
ReAct 的核心循环是:
思考 (Thought) → 行动 (Action) → 观察 (Observation) → 思考 → 行动 → ... → 最终回答
每一轮,LLM 先"想"一下当前应该做什么,然后选择一个 Tool 执行,拿到结果后再"想"下一步,直到它认为已经有足够的信息来回答用户。
整个过程中,LLM 拥有完全的自主权——选哪个工具、传什么参数、循环几次,全由模型自己决定。
原因很简单:ReAct 是最灵活的模式,几乎所有 Agent 教程的第一课都在教它。而且 LangGraph 直接提供了 createReactAgent 这个开箱即用的工具。我只需要定义好 Tool,把用户消息丢进去,Agent 就能"自动"工作了。
第一个:不可预测性。
用户说"帮我看看这个任务的进度",我期望 Agent 调用 query_task 查一下然后回答。但 ReAct 模式下,模型有时候会:先查任务详情,再查项目详情,再查团队成员负载,最后给出一个"综合分析"——用户只是想看个进度,不需要这么复杂的分析链路。更麻烦的是,同样的输入,不同时间可能走出不同的路径。
第二个:成本不可控。
ReAct 的循环次数由模型自己决定。一个简单的任务查询,模型可能"谨慎地"循环 4-5 轮(思考→调用→观察→再思考…),每一轮都在消耗 token。而其中很多轮次的"思考"是不必要的。
第三个:安全风险。
ReAct 模式下模型可以自由选择任何 Tool。我没有一个可靠的机制来确保"删除任务"这种高风险操作在执行前必须经过用户确认。只靠 prompt 里写"删除前请确认"是不够的——Anthropic 自己的工程建议里就明确说过:不能只靠 prompt 来防护高风险操作。
ReAct 的问题本质是:它把所有的决策权都交给了 LLM,但 LLM 并不是所有环节的最优决策者。 对于"查询任务进度"这种有明确固定流程的操作,我不需要 LLM 来"思考"应该怎么做——直接走固定流程就好。
这让我开始思考一个原则:确定性 workflow 优先,只有在真正需要开放式决策时才引入 LLM 推理。这条原则贯穿了后续所有的架构选择。
这个模式把 ReAct 的"边想边做"拆成了两个明确阶段:
Planner(规划)→ 生成步骤列表 → Executor(执行)→ 逐步执行 → 结果不对?→ Replanner(重新规划)
Planner 用一个强模型(比如 GPT-4 级别)来分析任务、生成完整的执行计划;Executor 用一个轻量模型或固定代码逐步执行计划;如果中间结果偏离预期,Replanner 会调整计划。
Plan-Execute 解决了 ReAct 的一部分不可预测性问题。因为计划是预先生成的,我至少可以在执行前看到 Agent 打算做什么,甚至可以让用户确认计划后再执行。这比 ReAct 的"黑盒循环"可控多了。
过度规划。
用户说"帮我创建一个任务:修复登录页面的样式"。这是一个再明确不过的指令——直接调用 create_task 就行了。但 Plan-Execute 模式下,Planner 仍然会被触发,花几秒钟"规划"一个只有一步的计划。这个规划过程消耗了一次强模型调用的成本和延迟,却没有产生任何价值。
所有请求走同一条路。
不管是"创建一个任务"(简单 CRUD)还是"帮我分析这个项目所有延期任务的原因"(需要深度推理),都要经过 Planner → Executor → Replanner 这条路径。简单操作被迫走了复杂链路,复杂操作的 Planner 又不够专业(它是通用的,不是针对特定业务场景优化的)。
Replanner 的引入增加了系统复杂度。
Replanner 需要判断"当前结果是否偏离预期",这本身就是一个模糊的判断。什么叫"偏离"?偏离多少要重新规划?重新规划几次就该放弃?这些都需要额外的设计和测试,而很多场景根本用不到 Replanner。
Plan-Execute 的问题在于:它假设所有请求都需要"规划"这个步骤,但现实中大量请求是可以直接执行的。 我需要一个机制来区分"需要规划的复杂请求"和"可以直接执行的简单请求"。
Router(路由器)的思路是:在 Planner 之前加一个轻量的分发层,先判断用户的请求属于哪个类别,然后根据类别走不同的处理路径。
用户输入 → Router(意图分类) ├─ 简单 CRUD → 直接执行(不经过 Planner) ├─ 数据查询 → SQL 查询 + 结果解释 ├─ 任务拆解 → Planner → Executor → Replanner └─ 周报生成 → 固定 Pipeline(取数→聚合→润色)
这解决了 Plan-Execute 的"所有请求走同一条路"问题——简单操作走快速路径,复杂操作才启用完整的规划链路。
进一步优化后,我把路由拆成了两层:
第一层:Fast-path Router(零 LLM 调用)。 前端的快捷按钮、卡片操作等结构化入口,直接映射到对应的能力模块,完全不需要 LLM 来判断意图。比如用户点了"生成周报"按钮——意图已经明确了,不需要模型来"理解"。
第二层:LLM Fallback Router(仅处理自由文本)。 只有用户在输入框里用自然语言提问时,才需要 LLM 来判断意图。而且这个 LLM 调用使用最轻量的模型,只做分类,不做推理。
这个两层设计的效果是:在我的场景中,预估超过 60% 的请求可以走 Fast-path,零 LLM 调用就完成路由。剩下 40% 的自由文本请求走轻量 LLM 路由,成本也远低于直接启动一个 ReAct 循环。
Router 分发解决了"不同请求走不同路径"的问题,但引入了一个新的问题:不同路径上的节点需要的上下文信息是不同的,但它们拿到的上下文是一样的。
具体来说,前端会传一个完整的页面上下文给后端,包含当前页面、选中的对象、筛选条件、可见的列表数据、分页信息等。这个上下文对象会被注入到 LangGraph 的 State 里,然后所有节点——Router、Executor、Tool、LLM prompt——都能读到全部内容。
刚开始我觉得这没什么问题:节点用得上的字段就用,用不上的就忽略呗。
但这个"忽略"的假设,很快就出事了。
事件一:Token 账单异常。
我在测试环境跑了一批模拟请求后发现,Router 节点消耗的 token 数远超预期。排查后发现,前端传过来的 visibleEntityIds(当前列表可见的条目 ID 列表)有时候多达 40-50 个 ID。这些 ID 被完整地塞进了 Router 的 prompt 里——但 Router 的工作仅仅是判断"用户想做什么类别的事",它根本不需要知道列表里有哪些具体条目。
每次请求白白多消耗几百 token,单次不多,但乘以日均请求量,一个月下来是一笔实际的成本。
事件二:Tool 里出现了不该有的 if-else。
我在 code review 时发现一段代码:某个 Tool 在执行查询时,根据 origin(请求来源)字段返回不同详细程度的结果——如果请求来自侧边栏就返回简要信息,来自 AI 工作台就返回详细信息。
这段代码"能跑",但它意味着 Tool 和 UI 入口产生了耦合。如果以后新增一个入口(比如从甘特图发起的分析),这个 Tool 就要再加一个 if 分支。而 origin 本来是路由层的信息,Tool 不应该关心请求从哪来,它只应该关心"操作什么数据"。
这段代码之所以能写出来,是因为 Tool 能读到 origin 字段——信息可达就会被使用,这是工程中的必然规律。
事件三:安全隐患。
我在考虑 prompt injection 防护时意识到,如果 LLM 的 system prompt 里直接拼入了完整的页面上下文 JSON,攻击者可以利用这些结构化信息构造更精准的注入指令。而 LLM 其实只需要一句自然语言描述就能理解当前场景——它不需要看到原始的数据结构。
这三件事的共同根源是:所有节点都能看到全部上下文,但没有任何机制约束"谁该看什么"。
我重新审视了整个 Agent 系统中的角色分工,发现有且只有三类上下文消费者:
1. 路由层(Router)。 它的任务是判断用户意图——'用户想做什么类别的事'。它需要知道用户在哪个页面、从哪个入口发起、选中了什么对象。它不需要知道列表有多长、筛选条件的细节、分页在第几页。
2. 执行层(Tool / Executor)。 它的任务是具体执行操作——查数据库、创建任务、修改状态。它需要知道操作的目标对象、筛选条件、分页信息。它不需要知道请求从哪个入口来——Tool 的行为不应该因为来源不同而改变。
3. 模型层(LLM system prompt)。 它的任务是理解场景并生成自然语言回答。它需要一个对当前页面的自然语言描述。它不需要原始的 JSON 结构、ID 列表、分页参数。
三类消费者,三种信息需求。那答案就很自然了:在信息进入 Agent 系统之前,按消费者裁剪成三份,每份只包含该消费者需要的字段。
这就是 Context Assembler——上下文装配器。它是后端的一个函数,在请求进入 Agent 图之前执行,把前端传来的完整页面上下文拆成三个视图:
前端传入:完整的页面上下文 │ Context Assembler(后端,执行一次) │ ┌───────┼───────────────┐ ▼ ▼ ▼ 路由上下文 执行上下文 模型上下文摘要 (RouterCtx) (ExecCtx) (ModelSummary) │ │ │ ▼ ▼ ▼ Router Tool/Executor LLM System Prompt
三份视图存入 Agent 的状态中,各节点只读取自己对应的那份。
字段分配的判断标准不是"这个字段对这一层有没有用",而是**'给了这个字段之后,这一层会不会做出不该做的事'**。
下面逐个分析关键字段的归属:
origin(请求来源)这是整个裁剪方案中最重要的隔离字段。
origin 表示请求从哪个 UI 入口发起:侧边栏、快捷按钮、任务行操作、AI 工作台等。
visibleEntityIds(当前列表可见条目)这是最容易造成 token 浪费的字段。
一个任务列表页可能同时展示 20-50 条任务的 ID。如果全量传入 Agent,每个 ID 大约 10-15 个字符,50 个 ID 就是 500-750 个字符,折合约 200 token。
filters(筛选条件)这个字段需要差异化处理。
完整的筛选条件可能包含:状态、负责人、优先级、日期范围、自定义字段等,结构比较复杂。
keyFilters。{"status":"in_progress","priority":"P1"},而是传"筛选:进行中、P1 优先级"。| 字段 | 路由层 | 执行层 | 模型层 | 判断理由 |
|---|---|---|---|---|
| page(当前页面) | 给 | 不给 | 给 (文字) | 路由需要页面信息;Tool 不关心在哪个页面;模型需要知道场景 |
| origin(请求来源) | 给 | 不给 | 不给 | 只有路由需要来源信息做 fast-path 映射 |
| selectedEntity(选中对象) | 给 | 给 | 给 (文字) | 三层都需要:路由判断目标、Tool 操作目标、模型描述目标 |
| routeParams(路由参数) | 给 | 给 | 给 (文字) | 同上,是定位信息 |
| activeTab(当前 Tab) | 给 | 不给 | 给 (文字) | 影响路由意图判断,不影响 Tool 执行 |
| viewMode(视图模式) | 不给 | 不给 | 给 (文字) | 只对描述场景有用('甘特视图') |
| filters(筛选条件) | 给精简版 | 给完整版 | 给摘要 | 三层需求粒度不同 |
| visibleEntityIds | 不给 | 有条件给 | 不给 | 最危险的膨胀源,严格控制 |
| searchKeyword | 不给 | 给 | 不给 | 纯执行参数 |
| pagination(分页) | 不给 | 给 | 不给 | 纯执行参数 |
关于模型层拿到的应该是原始 JSON 还是自然语言摘要,我做了一个明确的选择:只给自然语言摘要。
Token 效率差距明显。 同样的信息,JSON 格式大约 80 token,自然语言摘要大约 35 token。每次请求省一半,积少成多。
降低 Prompt Injection 风险。 结构化的 JSON 给了攻击者明确的格式线索。如果某个业务数据字段(比如任务标题)被注入了恶意内容,在 JSON 结构中它更容易被模型误解为系统指令。自然语言摘要打断了这种结构化格式,注入内容变成了一段不通顺的描述文字,模型更容易识别其异常。
LLM 的真实需求。 system prompt 里注入上下文的目的是让模型"了解当前场景",不是让模型"拿数据做计算"。真正需要用 ID 查数据库的是 Tool,不是 LLM。模型只需要知道"用户在项目详情页看着甘特图"就够了,projectId: "p123" 这种原始数据对它没有意义。
摘要的生成方式是纯模板拼接,不需要调 LLM。根据页面类型和存在的字段,用固定模板拼出一句话。比如:
'用户当前在项目详情页,项目 p123,Tab 进行中,甘特视图,选中了任务 t456,筛选:P1 优先级。'
控制在 200 token 以内,简洁明了。
做了裁剪只是第一步。三份上下文存入 Agent 状态后,还有一个问题:怎么保证后续节点不会篡改它们?
LangGraph 的状态更新机制是基于 reducer 的——每个状态字段都有一个 reducer 函数,决定"当节点返回新值时如何合并"。
我对三个上下文字段使用了"首次写入后冻结"的 reducer 策略:
这意味着即使某个节点的代码里不小心 return 了一个修改过的上下文对象,框架层面就会拒绝这次更新。架构约束下沉到了框架机制,而不是浮在代码规范上。 规范可以被忽略,框架机制不会。
再叠加 CI 层面的 grep 检查——比如路由模块的代码里不该出现执行上下文相关的关键词——三道防线(框架拒绝 → CI 报错 → Code Review)就构成了完整的保护。
回顾整个演进路径,每一次架构切换都不是"为了用新技术",而是上一个方案的某个缺陷在实际开发中暴露后的应对:
| 模式 | 核心思路 | 解决了什么问题 | 暴露了什么新问题 |
|---|---|---|---|
| ReAct | LLM 自主循环:思考→行动→观察 | 最灵活,能应对开放式问题 | 不可预测、成本不可控、安全风险高 |
| Plan-Execute | 先规划完整计划,再逐步执行 | 可预测性提升,可以预审计划 | 简单操作也走重型链路,过度规划 |
| Router 分发 | 先分类意图,不同类型走不同路径 | 简单走快路、复杂走慢路 | 所有节点共享全量上下文,信息越权 |
| Router + 三层裁剪 | 分类意图 + 按消费者裁剪上下文 | 信息隔离、成本控制、安全增强 | 当前方案(持续验证中) |
四次演进的底层逻辑是同一条线索:逐步收回 LLM 的权限,把确定性还给确定性的模块,把信息控制权从"消费者自律"转移到"供给侧管控"。
ReAct 是把所有决策权交给 LLM;Plan-Execute 是把执行权收回来但规划权还在 LLM;Router 是把路由决策权也尽量收回来(Fast-path 零 LLM);三层裁剪是把信息访问权也收回来(每个节点只看到该看的)。
每一步都在缩小 LLM 的"自由活动空间"——不是因为 LLM 不够强,而是因为在生产系统中,可控性比灵活性更重要。
一个架构方案的价值不在于它设计出来的那天有多漂亮,而在于三个月后、十个人协作时它还能不能保持住。
我在设计三层裁剪方案时,同步设计了一份"防滑坡检查表"。它列举了最可能发生的四种退化模式:
| 退化模式 | 为什么会发生 | 后果 |
|---|---|---|
| '为了赶进度,直接把完整上下文丢给规划节点' | 赶工期时最常见的"先跑通再优化" | 规划节点是 LLM 节点,全量上下文 = Token 浪费 + 注入面扩大 |
| '路由层里读取了列表数据来辅助意图判断' | 看似合理的"优化"——'列表为空时推荐创建' | 路由层开始承担业务逻辑,职责边界被打破 |
| 'Tool 根据请求来源返回不同格式的结果' | 需求驱动——'侧边栏要简洁版,工作台要详细版' | UI 入口和执行逻辑耦合,新增入口就要改 Tool |
| 'LLM prompt 里直接拼入了原始 JSON 筛选条件' | 图省事——'反正模型能理解 JSON' | Token 浪费,且暴露了不必要的数据结构 |
这份表的价值在于:它把"未来可能出的问题"变成了现在就可以检查的清单。 Code Review 时对照这张表过一遍,就能拦住大部分退化。
我认为,能预见系统会怎么腐化,比设计系统本身更能体现架构能力。任何架构在白板上画出来都是干净的,难的是在真实协作中保持住这个干净。
整个演进过程给我最大的感受是:Agent 系统的核心挑战不在于"让 LLM 更强",而在于"给 LLM 恰当的边界"。
初期我花了大量时间在 prompt 调优上——怎么让模型更准确地选择 Tool、更好地理解用户意图。但后来发现,很多问题的根源不是模型不够聪明,而是我给了它太多不该看的信息、太多不该有的权限。
三层上下文裁剪模式的本质就是一个在 LLM 系统中落地的"最小权限原则":
每个环节只应该看到刚好够它完成工作的信息,多一个字段都是负债。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online