LLM Agent上下文管理实战:微服务架构下的工具结果压缩与Context Server设计
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 服务返回 JSONB 服务返回 XMLC 服务返回纯文本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 预算下模型回答的准确率能提升一截因为留下来的都是真正有用的信息。