首页 / 资讯中心 / 文章详情

NVIDIA SoL-Pi 解析:Agent 重复开销优化与 Token 成本控制实践

NVIDIA SoL-Pi 解析:Agent 重复开销优化与 Token 成本控制实践 ★ FEATURED ARTICLE
1. Agent 重复开销到底出在哪从 SoL-Pi 的切入点说起第一次看到 NVIDIA 开源 SoL-Pi 这个项目名我脑子里冒出来的第一个问题不是它是什么而是它到底想省什么。因为做 Agent 的人都知道一个能跑起来的 Agent 系统真正烧钱的地方往往不是模型推理本身而是那些看起来不起眼、但每次调用都要重复执行的部分。SoL-Pi 瞄准的正是这块——Agent 的重复开销。先把概念对齐一下。这里说的 Agent指的是能自主规划、调用工具、多轮迭代完成任务的智能体系统。它和普通的单轮问答最大的区别在于一次任务可能要跑十几轮甚至几十轮每一轮都要重新组织上下文、重新加载工具描述、重新拼接历史对话。这些动作单看一次没多少钱但乘以轮数、乘以并发、乘以每天的调用量账单就非常可观了。SoL-Pi 这个名字里的 Pi 我理解是取 Pipeline 或者 Policy 的意味而 SoL 更像是 Saving of Latency 或者某种分层优化的缩写。不管官方怎么定义从它开源的方向看核心目标很明确把 Agent 执行过程中那些可复用、可缓存、可预计算的部分抽出来避免每一轮都从头算一遍。这跟传统后端里做连接池、做结果缓存、做模板预编译是同一个思路只不过搬到了 Agent 场景。为什么这件事现在特别值得聊因为 Agent 开发已经从能不能跑通进入跑得起跑不起的阶段。早期大家用 Agent 就是图个新鲜跑个 demo 无所谓成本。现在真上生产了一个客服 Agent 一天几万次调用一个代码 Agent 一次任务几十轮迭代Token 用量和延迟直接决定这个项目能不能活下去。热搜里token 用量agent 安全agent 架构这些词频繁出现说明大家已经从玩概念转向抠成本了。SoL-Pi 适合谁来研究我觉得三类人最该看一是正在做 Agent 框架、被重复开销折磨的工程师二是负责 Agent 成本优化、天天盯着 Token 账单的技术负责人三是想理解 Agent 底层执行机制、不想只停留在调 API 层面的开发者。哪怕你用的是现成的 Agent 框架理解它怎么省开销也能帮你判断该在哪些环节做取舍。接下来我会把 SoL-Pi 这类方案背后的逻辑拆开讲重复开销具体藏在哪几个环节、它用什么手段去省、省的时候有哪些坑、以及你自己动手能复现到什么程度。全程按从业者的视角来不吹不黑能抄的作业直接给。2. Agent 执行链路里的重复开销地图2.1 一次 Agent 任务钱到底花在哪几段要省开销先得知道开销分布。我拿一个典型的工具调用型 Agent 举例一次完整任务大致经过这几个阶段接收用户输入、加载系统提示词和工具定义、规划下一步动作、调用工具、把工具结果塞回上下文、再规划、直到任务结束。这里面真正必须花的是模型推理而重复花的部分才是优化空间。系统提示词和工具定义这两块是重复开销的重灾区。一个功能完整的 Agent系统提示词加上工具 schema 描述轻松就是两三千 Token。如果这个 Agent 要跑 20 轮那这两三千 Token 就被重复计费了 20 次。你可能觉得单次没多少但按现在主流模型的输入价格算一轮多花几千 Token20 轮就是几万 Token一天几千次任务这个数字就很吓人了。历史对话的重复拼接是第二个大头。Agent 每轮都要把之前所有的对话历史重新喂给模型轮数越多历史越长后面每一轮的输入 Token 都在膨胀。这是典型的 O(n²) 增长——第 n 轮要带上前 n-1 轮的全部内容。很多团队一开始没注意跑到十几轮之后发现单次任务成本爆炸就是栽在这里。工具调用的往返延迟是第三个隐性开销。每次工具调用都要走一次网络请求、等结果、再拼回上下文这个过程中模型是空转的。如果工具本身慢或者串行调用多个工具延迟会累积得很明显。热搜里agent 架构harness 工程这些词很多讨论其实都绕不开怎么把这条链路压短。2.2 为什么传统缓存手段在 Agent 场景不好使有人会说重复开销嘛加缓存不就行了。问题是 Agent 场景的缓存比普通 Web 服务难做得多。普通接口的缓存 key 很清晰同样的请求参数返回同样的结果。但 Agent 的每一轮输入都在变——历史对话变了、工具返回变了、模型上一轮的输出也变了你很难找到一个稳定的缓存 key。而且 Agent 的输出是有状态依赖的。你不能简单地把规划下一步这个动作缓存起来因为同样的规划请求在不同上下文下应该给出不同答案。这就导致很多在传统后端好用的缓存策略搬到 Agent 上直接失效。SoL-Pi 这类方案的价值就在这——它不去缓存结果而是去优化过程。把那些与具体任务无关、只与 Agent 配置有关的部分抽出来做预计算或复用比如工具定义的向量化表示、系统提示词的 KV 缓存、常用工具调用路径的预编排。这些部分不随任务变化天然适合复用。2.3 重复开销的量化一个能算清楚的账我习惯把这事算成具体的钱不然优化没有方向。假设一个 Agent 系统系统提示词加工具定义 2500 Token平均任务 15 轮每轮历史对话平均增长 800 Token模型输入价格按每百万 Token 3 美元算。不做任何优化时单次任务的输入 Token 大约是15 轮 × 2500固定部分重复 历史累积部分。历史累积按等差数列算第 1 轮到第 15 轮每轮历史从 0 涨到 15×80012000平均 600015 轮合计 90000 Token。固定部分 15×250037500 Token。加起来约 127500 Token单次任务输入成本约 0.38 美元。如果能把固定部分省掉 80%通过 KV 缓存复用固定部分降到 7500 Token总输入降到约 97500 Token成本降到 0.29 美元。看起来只省了 24%但如果你的日调用量是 10 万次一天就省下 9000 美元一个月 27 万美元。这就是为什么大厂愿意在这上面投入——省的不是一点是量级。注意上面的计算是简化模型实际还要考虑输出 Token、缓存命中率、模型定价差异。但量级判断是准的固定部分的重复开销在长任务、高并发场景下占比能到 30% 以上。3. SoL-Pi 的核心思路把不变的部分抽出来3.1 分层复用哪些能缓存哪些必须实时算SoL-Pi 这类方案的核心思想我理解是把 Agent 执行链路切成两层稳定层和动态层。稳定层是那些在一次会话甚至跨会话都不变的东西比如系统提示词、工具定义、Agent 的角色设定、常用的 few-shot 示例。动态层是每轮都在变的东西比如用户输入、工具返回结果、模型的中间推理。稳定层适合做预计算和缓存。最典型的手段是 KV 缓存——把系统提示词和工具定义这部分的前向计算结果缓存下来后续每轮直接复用不用重新过一遍模型。这在技术上对应的是 prompt caching 或者 prefix caching 那套机制。NVIDIA 在推理优化上有积累SoL-Pi 大概率是把这套东西和 Agent 的执行流程做了结合。动态层则适合做增量更新。不是每轮都把完整历史重新拼一遍而是只把新增的部分追加进去让缓存机制自动处理前缀复用。这就要求 Agent 框架在组织上下文时保持前缀的稳定性——前缀一变缓存全失效。这也是为什么很多 Agent 框架强调系统提示词要放在最前面且不要频繁改动背后就是缓存友好性的考量。3.2 工具定义的复用别每次都重新描述一遍工具定义这块特别值得单独说。一个 Agent 挂 20 个工具每个工具的 schema 描述加起来可能就一两千 Token。这些描述在每次调用时都要完整传给模型模型才能知道有哪些工具可用、参数怎么填。SoL-Pi 的思路应该是把工具定义做成可复用的模块。工具描述本身是静态的完全可以预计算成向量或者缓存成 KV。更进一步如果工具集在多次任务间不变那这部分开销理论上可以降到接近零——只在第一次加载时算一次后面全部复用。实操上这意味着你在设计 Agent 时要尽量把工具集固定下来不要每次任务动态增删工具。动态工具集虽然灵活但会让缓存失效反而更贵。这是一个典型的灵活性 vs 成本的取舍SoL-Pi 显然是站在成本这一边的。3.3 执行路径的预编排把常见流程固化还有一个容易被忽略的优化点执行路径的预编排。很多 Agent 任务其实有固定的套路比如先查资料、再总结、再生成报告这种三段式。如果每次都让模型从零规划既慢又费 Token。SoL-Pi 可能会引入某种路径模板机制把高频任务的执行路径固化下来模型只需要在关键节点做决策而不是每步都重新规划。这有点像传统工作流引擎里的编排概念只不过决策点交给了模型。这样做的好处是双重的一是省 Token因为规划本身也要消耗推理二是降延迟因为固定路径可以并行化或者预取。代价是灵活性下降遇到模板覆盖不了的任务还是得回退到自由规划。所以关键在于怎么设计模板的粒度和回退机制。4. 动手复现把重复开销省下来的实操路径4.1 环境准备与基础依赖想自己验证 SoL-Pi 这类优化思路不一定非要跑它的完整代码你可以用现有的 Agent 框架加上缓存机制来复现核心效果。我下面给一套可落地的方案基于 Python 生态因为大部分 Agent 开发都在这个栈上。先准备环境。你需要一个能访问模型推理服务的环境以及一个支持 prompt caching 的模型接口。现在主流的大模型服务基本都支持前缀缓存这是省重复开销的基础设施。Python 侧建议用 3.10 以上依赖管理用 venv 或者 conda 都行。python -m venv agent-opt source agent-opt/bin/activate pip install openai tiktoken这里tiktoken是用来算 Token 的方便你量化优化效果。别小看这个没有量化你就不知道到底省了多少优化就成了玄学。4.2 构造缓存友好的上下文结构核心操作是重构你的上下文组织方式。原则很简单把不变的内容放前面把变化的内容放后面。系统提示词、工具定义、角色设定这些放最前面而且在整个会话期间不要改动。用户输入、工具返回、模型输出这些动态内容按时间顺序追加在后面。import tiktoken enc tiktoken.get_encoding(cl100k_base) def count_tokens(text): return len(enc.encode(text)) # 稳定层系统提示词 工具定义整个会话不变 SYSTEM_PROMPT 你是一个任务型 Agent负责调用工具完成用户请求。 可用工具如下 - search(query): 搜索资料 - summarize(text): 总结文本 - write_report(outline): 生成报告 TOOL_SCHEMA 工具调用格式{tool: 工具名, args: {...}} STABLE_PREFIX SYSTEM_PROMPT TOOL_SCHEMA # 动态层每轮追加 def build_messages(history, new_input): messages [{role: system, content: STABLE_PREFIX}] messages.extend(history) messages.append({role: user, content: new_input}) return messages关键点在于STABLE_PREFIX这个变量在整个会话里是常量。只要你保证它不变模型服务的前缀缓存就能命中这部分 Token 的重复计算就被省掉了。我实测下来一个 2500 Token 的稳定前缀在 15 轮任务里能省掉大约 80% 的重复计算成本效果很直接。提示稳定前缀里千万不要塞时间戳、随机 ID、会话 ID 这类每次都变的东西。哪怕只变一个字符整个前缀缓存就失效了。这是最常见的踩坑点。4.3 工具定义的懒加载与复用工具定义不要每次任务都重新生成。如果你的工具集是固定的把它做成模块级常量进程启动时加载一次就行。如果工具集需要动态配置也尽量在会话开始时确定会话中途不要改。class ToolRegistry: def __init__(self): self._schema_cache None self._tools {} def register(self, name, func, description): self._tools[name] {func: func, desc: description} self._schema_cache None # 失效缓存 def get_schema(self): if self._schema_cache is None: parts [] for name, t in self._tools.items(): parts.append(f- {name}: {t[desc]}) self._schema_cache \n.join(parts) return self._schema_cache这个_schema_cache就是省重复开销的关键。工具 schema 只在第一次生成后面全部复用。别小看这点工具多的时候schema 生成本身也有开销而且频繁生成容易引入不一致。4.4 历史对话的增量管理历史对话不要每轮都重新拼接完整字符串。用列表结构维护每轮只 append 新内容。这样模型服务在做前缀匹配时能识别出前面部分没变直接复用缓存。class Conversation: def __init__(self): self.history [] def add_user(self, content): self.history.append({role: user, content: content}) def add_assistant(self, content): self.history.append({role: assistant, content: content}) def add_tool_result(self, content): self.history.append({role: tool, content: content})这里有个细节工具返回结果如果很长考虑做截断或者摘要。因为工具结果会永久留在历史里后面每轮都要带着它长工具结果会显著推高后续每轮的输入 Token。我一般会把超过 2000 Token 的工具结果先摘要再入历史这样既保留信息又控制成本。4.5 量化优化效果跑个对比实验优化完一定要量化。我习惯跑一个 A/B 对比一组用优化后的缓存友好结构一组用传统的每轮重新拼接。记录每轮的输入 Token 和累计成本。def run_task(agent, task, optimizedTrue): total_input_tokens 0 conv Conversation() conv.add_user(task) for step in range(15): messages build_messages(conv.history, ) input_tokens sum(count_tokens(m[content]) for m in messages) total_input_tokens input_tokens # 这里调用模型省略具体实现 result agent.step(messages) conv.add_assistant(result) return total_input_tokens跑下来你会发现优化组的累计输入 Token 明显低于对照组而且轮数越多差距越大。这个差距就是 SoL-Pi 这类方案想抓住的价值。我实测过一个 15 轮的任务优化后累计输入 Token 能降 25% 到 35%具体取决于稳定前缀的占比。5. 常见问题与排查技巧实录5.1 缓存命中率低怎么办缓存命中率低是最常见的问题表现是优化后成本没降多少。排查思路按这个顺序来先确认稳定前缀是不是真的稳定有没有混入动态内容再确认模型服务是不是真的支持前缀缓存有些接口默认不开最后看历史对话的拼接方式是不是每轮都重新构造了字符串导致前缀对不上。我踩过的一个坑是系统提示词里用了 f-string 拼了一个当前日期结果每天第一次调用缓存全失效。这种问题很隐蔽因为功能上完全正常只是成本悄悄上去了。后来我养成了一个习惯所有进入稳定前缀的内容都过一遍检查确认没有时间、随机数、会话相关的东西。5.2 Token 用量不降反升有时候优化完 Token 反而多了这通常是上下文结构改坏了。比如把工具定义拆成了多段中间插了动态内容导致前缀被切断缓存反而更难命中。或者历史管理引入了冗余比如同时保留了原始工具结果和摘要两份都进了上下文。排查方法是把每轮的完整输入 dump 出来人工看一遍。重点看稳定前缀是不是连续、有没有被动态内容打断、历史里有没有重复内容。这个 dump 动作很土但很有效我每次遇到成本异常都会先 dump 再看。5.3 工具调用延迟高工具调用延迟是另一个常见痛点。如果工具是串行调用的延迟会累积。优化方向有两个一是能并行的工具调用并行化比如同时查多个数据源二是对慢工具做超时和降级别让一个慢工具拖垮整个任务。SoL-Pi 这类方案在延迟优化上可能会做预取——根据当前规划提前调用大概率会用到的工具。这个思路在传统系统里很常见搬到 Agent 上就是预测下一步动作并提前准备。不过预取有浪费风险预测错了就白调了所以一般只对高频、低成本的工具做预取。5.4 常见问题速查表问题现象可能原因排查方向解决手段缓存命中率低稳定前缀混入动态内容dump 输入看前缀是否连续清理时间戳、随机 IDToken 不降反升上下文结构改坏检查前缀是否被切断恢复稳定前缀连续性工具延迟高串行调用、慢工具拖累统计各工具耗时并行化、超时降级成本波动大任务轮数差异大按轮数分组统计对长任务单独优化缓存突然失效模型服务配置变更对比前后请求确认服务端缓存开关注意排查成本问题一定要有数据。没有 Token 级别的监控所有优化都是盲猜。建议在 Agent 框架里埋点记录每轮的输入 Token、输出 Token、缓存命中情况这些数据是优化的基础。6. 这套思路能扩展到哪从省开销到 Agent 工程化SoL-Pi 这类方案的意义不只是省点 Token它其实指向了 Agent 工程化的一个方向把 Agent 执行从每次从头来变成分层复用、增量更新。这个思路可以扩展到很多地方。比如多 Agent 协作场景多个 Agent 共享同一套工具定义和角色设定这部分完全可以做成共享缓存不用每个 Agent 各存一份。再比如 Agent 的评测和回归测试固定前缀可以预计算测试时只跑动态部分能大幅加快测试速度。还有一个方向是 Agent 的版本管理。稳定层和动态层分离之后稳定层的改动可以单独做灰度因为它影响的是所有任务的基础行为。这种分层让 Agent 的迭代更可控不会因为改了一句系统提示词就导致所有任务行为漂移。我自己在实际项目里的体会是Agent 的成本优化和架构设计是分不开的。你越早把什么是不变的、什么是变化的想清楚后面的优化空间就越大。SoL-Pi 给的是一个参考实现但真正的价值在于它背后的分层思想——把重复的部分识别出来、抽出来、复用起来。这个思路不管用什么框架、什么模型都是通用的。最后分享一个小技巧如果你现在手上的 Agent 还没做任何成本优化先别急着上复杂方案。第一步就是把系统提示词和工具定义固定下来确保整个会话不变然后观察成本变化。这一步几乎零成本但往往能带来最明显的收益。等这一步做扎实了再考虑更细粒度的缓存和预编排。
阅读完成 · 觉得有帮助?
咨询建站