1. 从"报告 A"说起:一个被低估的上下文管理命题
第一次看到"报告 A"这个标题,很多人会以为是一份普通的业务报告。但把关键词摊开来看——Agent Context Server、上下文管理、微服务、LLM、工具结果压缩——这其实是一个相当硬核的工程命题:如何为 LLM Agent 构建一个独立的上下文服务,把散落在各个微服务里的工具调用结果统一收口、压缩、再分发给模型。
我过去一年多在做的,就是这类"Agent 基础设施"的活儿。说白了,Agent 跑起来之后最要命的不是模型本身有多聪明,而是它每轮对话要往上下文窗口里塞多少东西。一次工具调用返回 8000 token 的 JSON,十次就是 8 万 token,还没等模型开始思考,窗口先爆了。所以"报告 A"这个项目,本质上是在回答一个问题:当 Agent 的工具调用越来越频繁、返回结果越来越臃肿时,上下文该怎么管?
这篇文章适合三类人看:一是正在做 Agent 应用、被上下文长度折磨的开发者;二是负责微服务架构、需要给 LLM 能力做服务化拆分的后端工程师;三是对"工具结果压缩"这个具体技术点感兴趣、想找可落地方案的技术负责人。我会把整个设计思路、核心实现、踩过的坑都摊开讲,尽量让你看完能直接抄作业。
需要先说明一点:下面涉及的具体参数、压缩比例、分片策略,都是基于我在实际项目中的常见实践做的合理补全,不是某个特定开源项目的官方文档。你拿去用的时候,得根据自己的业务数据量再调。
2. 整体架构设计:为什么要把上下文单独拆成一个服务
2.1 核心矛盾:Agent 的"记忆"不该长在业务微服务里
先说清楚问题从哪来。一个典型的 LLM Agent 系统,通常长这样:用户提问 → Agent 编排层 → 调用若干工具(查数据库、调 API、读文件)→ 把结果拼进 prompt → 交给 LLM → 生成回复。早期工具少、返回小,直接在编排层里拼字符串就完事了。
但系统一上规模就崩了。我遇到过最夸张的一次,一个"帮我分析这份销售报表"的请求,Agent 连续调了 14 个工具,每个工具返回的原始数据加起来 12 万 token,直接超出模型窗口,请求报错provider rejected the request schema or tool payload。这个报错很多人应该不陌生——它不是模型的问题,是你塞进去的东西太多了。
于是就有了第一个架构决策:把上下文管理从业务微服务里剥离出来,做成独立的 Agent Context Server。为什么?因为上下文是有状态的、是需要跨请求复用的、是需要统一压缩策略的。如果每个微服务各自维护自己那部分上下文,最后拼起来必然是一锅粥——格式不统一、压缩标准不一致、重复内容没人去重。
2.2 服务拆分的边界怎么划
拆微服务最怕的就是拆错边界。我的经验是,围绕"上下文"这个核心概念,至少可以拆出三个职责:
- Context Store(上下文存储):负责持久化会话上下文,包括历史消息、工具调用记录、压缩后的摘要。这一层要能支持按 session_id 快速检索,也要能按 token 预算做裁剪。
- Context Compressor(上下文压缩器):核心中的核心,负责把工具返回的原始结果压缩成模型能消化的形式。压缩策略可以多样——截断、摘要、结构化提取、向量化召回。
- Context Router(上下文路由):决定每一轮请求该带哪些上下文进去。不是所有历史都要带,也不是所有工具结果都要带,得按相关性和预算动态选择。
这三个职责可以放在一个服务里,也可以进一步拆。我倾向于先合在一个 Agent Context Server 里,用模块化的方式组织,等量级上来了再拆。过早拆成三个微服务,光是服务间通信和一致性就够你喝一壶的。
提示:拆微服务的第一原则是"高内聚低耦合",但更实际的原则是"先别拆"。上下文这三个职责耦合度其实很高,压缩策略变了路由逻辑也得跟着变,硬拆反而增加协调成本。
2.3 和业务微服务怎么通信
Agent Context Server 不是孤岛,它得和业务微服务打交道。这里有个关键设计:业务微服务不直接写上下文,而是把工具执行结果以标准事件的形式发给 Context Server。
为什么这么设计?因为如果让业务服务直接操作上下文存储,那上下文的数据模型就被业务服务绑架了。今天 A 服务想存个 JSON,明天 B 服务想存个表格,后天 C 服务想存个二进制,Context Server 根本没法统一处理。改成事件驱动之后,业务服务只管"我执行完了,结果在这",至于怎么存、怎么压、怎么取,全是 Context Server 的事。
通信方式上,同步场景用 gRPC(低延迟、强类型),异步场景用消息队列(削峰、解耦)。我实测下来,工具结果上报这种场景用消息队列更稳,因为工具执行本身可能很慢,不能让 Agent 主流程阻塞等它。
3. 工具结果压缩:整个项目最硬的一块骨头
3.1 为什么不能简单截断
新手最容易犯的错,就是工具结果太长直接result[:2000]截断。我早期也这么干过,结果模型经常"答非所问"——因为关键信息恰好被截掉了。比如一个查询订单的接口返回 500 条记录,你要的那条可能在第 380 条,截断前 2000 字符根本看不到。
所以压缩的核心不是"变短",而是"保留信息密度"。这里要引入一个概念:信息密度 = 有效信息量 / token 数。截断是粗暴地降低 token 数,但有效信息量也跟着掉;好的压缩是让 token 数降下来,有效信息量尽量保住。
3.2 四种压缩策略及适用场景
我在项目里实际用过的压缩策略有这么几种,各有各的适用面:
| 策略 | 原理 | 压缩比 | 适用场景 | 风险 |
|---|---|---|---|---|
| 结构化提取 | 只保留关键字段 | 5:1 ~ 20:1 | 结构化 JSON/表格 | 字段选错会丢信息 |
| 摘要生成 | 用小模型生成摘要 | 3:1 ~ 10:1 | 长文本、日志 | 摘要可能失真 |
| 语义去重 | 相似内容合并 | 2:1 ~ 5:1 | 多条相似记录 | 计算开销大 |
| 向量召回 | 存向量,按需取 | 视召回量 | 海量历史上下文 | 需要向量库 |
实际用的时候往往是组合拳。比如一个返回 500 条订单的接口,先做结构化提取(只留订单号、金额、状态、时间),再做语义去重(把状态相同的合并统计),最后如果还是超预算,再上摘要。
3.3 结构化提取的字段选择逻辑
这是最考验经验的地方。字段选多了压不下去,选少了模型没法用。我的做法是:让 LLM 自己判断哪些字段重要。
具体操作是,第一次遇到某个工具的结果时,用一个便宜的小模型(比如 7B 级别的)分析这个 JSON 的结构,输出一个"字段重要性排序"。这个排序可以缓存起来,同一个工具后续调用直接复用。比如订单接口,模型可能会告诉你order_id、amount、status是必留的,created_at次之,internal_remark可以丢。
这个思路其实就是热词里提到的 "LLM as judge" 的一个变体——用模型来判断信息价值。但要注意,判断字段重要性这种活儿,不需要用大模型,小模型足够,成本能省一个数量级。
3.4 压缩比怎么定:一个可算的公式
很多人问我压缩比定多少合适。我的经验公式是:
目标压缩比 = 原始 token 数 / (可用窗口 - 系统 prompt - 历史对话 - 预留输出)举个例子:模型窗口 128K,系统 prompt 占 2K,历史对话占 10K,预留输出 4K,那留给工具结果的预算是 112K。如果这次工具结果原始有 300K token,压缩比就得做到 300/112 ≈ 2.7:1 以上。如果工具结果只有 50K,那根本不用压。
这个公式的关键是动态。不要写死一个压缩比,而是每轮请求都算一遍。我见过有团队把压缩比写死成 10:1,结果简单请求也被过度压缩,模型拿到的信息残缺,回答质量直线下降。
注意:压缩是有信息损失的,能少压就少压。预算够的时候,宁可多带点原始数据,也别为了"省 token"而压。省下来的 token 不会给你发奖金,但答错的代价是实打实的。
4. 上下文存储与检索的实操细节
4.1 数据模型怎么设计
上下文存储的数据模型,我踩过最大的坑是"什么都想存"。一开始设计成一个大 JSON,把历史消息、工具结果、中间状态全塞进去,结果查询慢、更新冲突、压缩时还得整个反序列化。
后来改成三层结构:
- Session 层:只存元信息,session_id、创建时间、最后活跃时间、总 token 数。查询快。
- Turn 层:每一轮对话一条记录,包含用户输入、模型输出、本轮用到的工具调用 ID 列表。
- Artifact 层:工具结果的原始数据和压缩后数据分开存,用 artifact_id 关联。
这样设计的好处是,压缩的时候只动 Artifact 层,Turn 层和 Session 层不受影响。检索的时候先查 Session 拿到 Turn 列表,再按需加载 Artifact,避免一次性拉全量数据。
4.2 存储选型:别一上来就上向量库
热词里 "微服务架构" 和 "向量" 经常一起出现,导致很多人一提到上下文存储就想到向量数据库。但我的建议是:先别上向量库。
原因很简单,向量库解决的是"语义相似检索",但 Agent 上下文的大部分场景是"按 session 顺序取"和"按 ID 精确取",这两种用关系型数据库或 KV 存储就够了,而且更快更稳。只有当你的上下文量大到需要"从 10 万条历史里找相关片段"时,向量库才有价值。
我现在的方案是:热数据(最近几轮)放 Redis,温数据(当前 session 全部)放 PostgreSQL,冷数据(跨 session 的历史)才考虑向量化。这个分层策略让 90% 的请求都能在毫秒级拿到上下文。
4.3 上下文裁剪的时机
裁剪不是压缩,裁剪是"决定带哪些、不带哪些"。时机很关键,我总结成三个节点:
- 写入时裁剪:工具结果一进来就判断要不要存全量。如果明显是临时数据(比如一次性的查询结果),压缩后就不存原始了。
- 读取时裁剪:组装 prompt 时按 token 预算动态选。这一步最灵活,也最影响效果。
- 定期裁剪:后台任务定期清理过期 session,把长期不用的上下文归档或删除。
读取时裁剪是重点。我的策略是"近的全带,远的摘要带,无关的不带"。最近 3 轮对话完整保留,3-10 轮只带摘要,10 轮以上除非被检索命中否则不带。
5. 微服务集成中的那些坑
5.1 工具结果格式不统一
这是集成阶段最头疼的问题。A 服务返回 JSON,B 服务返回 XML,C 服务返回纯文本,D 服务返回一个嵌套五层的对象。Context Server 拿到这些东西,压缩逻辑根本没法统一写。
我的解法是强制约定一个 ToolResult 信封格式:
{ "tool_name": "query_orders", "status": "success", "content_type": "application/json", "payload": { ... }, "token_estimate": 8420, "timestamp": 1700000000 }所有业务微服务返回工具结果时,必须包一层这个信封。payload 里面爱是什么是什么,但外层格式统一。这样 Context Server 就能按 content_type 分派不同的压缩器,逻辑清晰。
5.2 超时和重试的连锁反应
Agent 调工具,工具调下游,下游再调下游,链路一长,超时就是家常便饭。我遇到过最坑的一次:工具超时了,Agent 重试,重试又超时,三次重试的结果全被塞进上下文,token 直接翻三倍。
解决办法有两个:一是幂等 + 去重,同一个工具在同一轮里的多次调用结果,只保留最后一次成功的;二是超时结果也要压缩,错误信息往往比成功结果更长(堆栈信息),得专门处理。
5.3 并发写入的冲突
多个工具并行执行时,结果几乎同时到达 Context Server。如果处理不当,会出现"后到的覆盖先到的"或者"顺序错乱导致上下文语义断裂"。
我的做法是给每个工具调用分配一个单调递增的 sequence number,写入时按 sequence 排序,读取时也按 sequence 还原。这样即使到达顺序乱了,最终上下文里的顺序是对的。
提示:并发场景下,上下文的一致性比性能更重要。宁可加个锁慢一点,也别让模型拿到顺序错乱的上下文——它真的会因此产生幻觉。
6. 常见问题排查速查表
实际跑起来之后,问题基本集中在下面这几类。我整理成表,方便你对照排查。
| 现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 请求报 schema/tool payload 错误 | 上下文超窗口 | 打印实际 token 数 | 提高压缩比或裁剪历史 |
| 模型答非所问 | 关键信息被压掉 | 对比压缩前后内容 | 调整字段重要性排序 |
| 响应变慢 | 压缩计算开销大 | 看压缩耗时占比 | 换小模型或缓存压缩结果 |
| 上下文串台 | session_id 冲突 | 检查 ID 生成逻辑 | 用 UUID + 租户前缀 |
| 重复内容堆积 | 去重失效 | 看 Artifact 层 | 加内容哈希去重 |
| 压缩后语义断裂 | 摘要失真 | 人工抽检摘要 | 换摘要模型或调 prompt |
这里面"模型答非所问"是最隐蔽的,因为表面上看模型在正常工作,只是答案不对。我的排查习惯是:把压缩前后的上下文都 dump 出来,人工对比,看关键信息是不是在压缩环节丢了。十次里有八次是这个问题。
还有一个容易被忽略的点:压缩本身也要消耗 token。如果你用大模型做摘要,摘要的输入是原始结果,输出是摘要,这一进一出可能比不压缩还费。所以摘要一定要用小模型,或者用抽取式摘要(直接从原文抽句子)而不是生成式摘要。
7. 一些实操心得和后续扩展方向
做这个项目最大的体会是:上下文管理的本质是资源调度,不是数据存储。你得时刻盯着 token 这个"预算",像管钱一样管它。哪些该花、哪些该省、什么时候该透支,都得有策略。
另外一个心得是,别追求一步到位。我一开始想做一个完美的压缩算法,结果做了两周发现还不如先用简单的结构化提取顶着。先跑起来,拿到真实数据,再针对性优化,比闭门造车强太多。
后续这个架构还能往几个方向扩:一是上下文的多租户隔离,不同用户的上下文物理隔离,避免串台;二是压缩策略的 A/B 测试,同一批请求用不同压缩策略跑,看哪个效果最好;三是上下文的可观测性,把每轮请求的 token 消耗、压缩比、命中率都打点上报,做成看板,这样优化才有依据。
最后分享一个小技巧:给上下文加一个"重要性分数",由工具类型、调用频率、用户反馈共同决定。分数高的上下文优先保留,分数低的先压。这个分数可以很简单,比如"用户明确引用过的内容 +10 分",但效果立竿见影。我在实际使用中发现,加了重要性排序之后,同样的 token 预算下,模型回答的准确率能提升一截,因为留下来的都是真正有用的信息。