免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

SKILL.state:用显式执行状态破解Agent上下文膨胀难题

SKILL.state:用显式执行状态破解Agent上下文膨胀难题 做 Agent 应用开发时大家几乎都遇到过这样的场景一个多步骤任务从开始到结束模型每一步的思考、动作、观察结果全部追加到上下文里。任务越长上下文越长token 费用越来越高响应越来越慢甚至到后面模型开始“忘事”或答非所问。最近 Google 研究团队提出的 SKILL.state 给出了一个很直接的新视角与其让智能体把越来越多的对话历史塞进上下文不如维护一份“显式执行状态”让模型按需读取状态、更新状态而不是把过去每一轮的完整轨迹都重新读一遍。这个思路对正在做智能体平台、工作流编排、多步骤任务自动化的开发者来说非常值得深入研究。本文会先讲清楚 SKILL.state 解决的核心问题再拆解它的设计思路然后用 Python 写一个极简的状态型 Agent 原型最后聊聊真实工程中的落地方法和常见误区。适合哪些读者正在做 Agent 开发、智能体工作流搭建、RAG 应用集成以及对 LLM 上下文优化感兴趣的开发者。读完你可以掌握“显式执行状态”这个设计模式并把它应用到自己的智能体项目中。1. 智能体的对话历史为什么会不断膨胀1.1 ReAct 模式下的“每一步都进上下文”当前大多数 LLM Agent 采用类似 ReAct 的循环结构模型先思考Thought再决定调用某个工具Action工具返回结果Observation模型再基于结果继续思考。这个循环会一直持续到任务完成。在这个过程中有一个很容易被忽略的默认行为每一步的思考、动作、工具返回结果都会被追加到对话历史中作为下一轮模型输入的上下文。例如一个简单的“查询订单并办理售后”任务执行流程可能是用户说帮我查一下订单 A1001 的配送状态。Agent 调用查询接口返回一段 JSON。Agent 根据 JSON 回复用户。用户又说如果已经签收帮我申请售后。Agent 再次调用售后接口返回处理结果。如果每一步都保留原始内容当任务执行到第 20 步、第 50 步时上下文里堆积的内容会非常可观。尤其工具返回的原始 JSON、长文本、日志等往往占据大量 token但真正对下一步决策有用的可能只有其中几个字段。1.2 上下文膨胀带来的三个代价对话历史越来越长问题不只是“多花点钱”它会在三个层面影响系统成本变高。输入 token 随历史长度线性增长长任务跑一次可能消耗上万甚至几十万 token按量计费模式下成本压力非常大。延迟变高。模型处理超长输入需要更长的 prefill 时间用户等待时间随之增加。对实时交互型智能体来说这种延迟很影响体验。效果变差。研究表明LLM 在处理超长上下文时存在“中间信息丢失”问题模型可能会忽略关键细节或者被无关历史干扰导致错误决策。上下文窗口再大也不意味着模型真的能“用得好”。1.3 一个关键直觉Agent 到底需要什么我们不妨停下来思考一个问题一个执行到第 30 步的 Agent它要做出第 31 步决策时真的需要第 1 步到第 30 步的完整记录吗大多数情况下不需要。它真正需要的是这些信息任务目标是什么。当前已经完成了哪些子任务。还有哪些子任务待完成。当前关键变量值是多少如订单号、用户 ID、金额、状态。最近一次操作是否成功。这些信息本质上就是“执行状态”。完整对话历史只是状态的“事件日志”而状态本身才是决策的核心依据。SKILL.state 的思路就是把“每一轮都重新输入完整历史”替换为“维护一份显式的、结构化的执行状态”。2. SKILL.state 核心思想解读2.1 用显式执行状态替代对话历史根据论文公开信息SKILL.state 的核心主张是在智能体循环中引入显式、可读、可更新的执行状态execution state用它替代不断增长的对话历史让模型基于当前状态进行决策。具体来说传统智能体循环的输入是“历史消息列表”每一轮都把所有消息重新作为上下文输入。而 SKILL.state 风格的智能体循环则变成初始化一个结构化状态对象例如任务 ID、目标、已完成步骤、关键字段、待办列表。每一轮决策前把当前状态序列化为紧凑文本交给模型。模型输出动作智能体执行工具。工具结果不直接全部追加到上下文而是先经过解析更新状态对象中的关键字段。下一轮继续基于更新后的状态决策。这样的好处是上下文长度不再随任务执行步数线性增长而是基本保持稳定只取决于状态对象的大小。2.2 状态与历史的本质区别为了准确理解这个设计需要区分“历史”和“状态”这两个概念对话历史描述“发生了什么”是一条按时间排列的事件流。它记录了用户说过什么、Agent 说过什么、工具返回过什么。执行状态描述“现在是什么”是任务在某个时刻的完整画像。它只保留当前对后续决策有意义的信息。举一个现实中的类比你让一个实习生处理客户投诉。一种方式是让他把和客户沟通的每一句话、每一次查数据库的记录全部背下来另一种方式是让他维护一张表格表格里写着客户姓名、订单号、问题类型、当前处理进度、下一步计划。显然第二种方式更高效。SKILL.state 本质上就是把 Agent 从“背对话记录”转变为“维护业务表格”。2.3 按需读取而不是全量扫描论文思想中还有一个重点是“按需访问”。模型不需要在每一轮都看到完整状态的所有字段而是可以根据当前动作需要只读取相关部分。这就进一步压缩了输入长度。举个例子一个订单处理 Agent 的状态可能包含用户信息、商品明细、支付记录、物流信息、售后进度等多个模块。当模型需要查询物流时只需要读取物流相关字段而不需要把整个订单的所有明细都放进去。这种设计也符合软件工程里的“关注点分离”原则状态按模块拆分模型决策时按需拼接。3. 为什么状态比历史更合适3.1 信息密度与可更新性完整历史的信息密度很低。一条工具返回的原始 JSON 可能有一千个 token但其中真正改变状态的可能只有一个 status 字段。而显式状态只保留“变化后的结果”信息密度高得多。同时历史是只增不改的如果某一步执行出错或者工具返回了错误结果错误信息会永久留在历史里后续每一轮模型都要“忍受”这些噪音。而显式状态是可更新的某一步出错后可以修正状态字段下一轮模型看到的即为修正后的干净状态。3.2 可验证、可审计、可规划显式状态还有一个重要优势它是结构化、可读取的因此可以在工程层面对其做校验和审计。比如一个订单售后 Agent如果要判断“用户是否已经完成支付”可以直接检查状态中的payment_status字段而不需要让模型从一大段历史中推理出来。这降低了模型误判的概率。同时规划器Planner可以直接基于状态生成下一步计划。因为状态里已经明确列出“已完成”“待办”“阻塞项”规划器不需要从历史中抽取这些信息。3.3 对照常见方案的取舍为了看得更清楚可以把几种常见上下文管理方案放在一起对比方案核心做法优点缺点完整历史每轮把所有消息输入模型信息完整、实现简单成本高、延迟高、长上下文效果差滑动窗口只保留最近 N 轮消息控制长度会丢失早期关键信息历史摘要定期把旧历史总结成摘要减少 token摘要会丢失细节且摘要本身占用上下文向量记忆检索把历史存入向量库按需检索可扩展检索不精准时会引入噪音仍需写回上下文显式执行状态维护结构化状态每轮基于状态决策信息密度高、可控、可审计需要设计状态结构可能丢失上下文细节从表中可以看出显式执行状态并不是“银弹”它是用“状态建模成本”换取“决策效率和稳定性”。在长任务、重复执行、步骤明确的场景中收益非常明显。4. 概念验证用 Python 实现一个极简状态型 Agent下面我用一个极简示例演示 SKILL.state 的核心思想。这个示例不使用真实 LLM而是用规则模拟 LLM 决策重点展示“状态驱动决策”的循环结构以及上下文长度如何保持稳定。4.1 场景设计我们模拟一个订单处理 Agent任务流程是根据用户提供的订单号查询订单信息然后计算订单金额最后发送处理通知。整个流程需要三步工具调用。传统 ReAct 模式下三步执行后上下文里会堆积查询结果 JSON、中间推理等大量内容。状态型模式下每一步只把“当前状态文本”交给决策模块。4.2 定义执行状态首先定义订单任务的状态结构# 文件路径agent_state_demo/state_agent.py from __future__ import annotations from dataclasses import dataclass, field from typing import Dict, Any dataclass class OrderTaskState: task_id: str order_id: str user_id: str order_info: Dict[str, Any] field(default_factorydict) total_cost: float 0.0 step_count: int 0 status: str pending finished: bool False error: str def to_prompt(self) - str: 将状态序列化为紧凑文本替代完整对话历史。 return ( f任务ID: {self.task_id}\n f状态: {self.status}\n f订单号: {self.order_id or 未确定}\n f用户ID: {self.user_id or 未确定}\n f订单信息: {self.order_info if self.order_info else 尚未查询}\n f订单金额: {self.total_cost}\n f已执行步骤: {self.step_count}\n f错误信息: {self.error or 无}\n )这里的关键是to_prompt()方法它把状态对象变成模型输入文本。无论执行了多少步这个文本的长度基本是稳定的只取决于状态字段数量而不是执行步数。4.3 模拟工具层接下来定义一组工具函数工具执行后把结果更新到状态中而不是把原始结果追加到历史def query_order(state: OrderTaskState, order_id: str) - Dict[str, Any]: 模拟查询订单接口。 return { order_id: order_id, user_id: U_10086, amount: 199.0, status: paid, } def calculate_cost(state: OrderTaskState) - float: 模拟计算订单金额。 return state.order_info.get(amount, 0.0) def send_notice(state: OrderTaskState) - str: 模拟发送通知。 return f已通知用户 {state.user_id}订单 {state.order_id} 处理完成4.4 模拟决策模块在真实项目中这里的决策模块应该调用 LLM。本文为了示例可运行使用规则模拟模型决策根据状态文本里是否存在某个关键信息决定下一步动作。def llm_decision(state: OrderTaskState) - Dict[str, Any]: 模拟 LLM 决策读取当前状态输出下一个动作。 if not state.order_id: return {action: query_order, params: {order_id: A1001}} if not state.order_info: return {action: query_order, params: {order_id: state.order_id}} if state.total_cost 0.0: return {action: calculate_cost, params: {}} if state.status ! notified: return {action: send_notice, params: {}} return {action: finish, params: {}}可以看到决策函数每次都只接收状态对象不接收历史。它通过检查“当前状态缺什么”来决定“下一步做什么”这正是状态驱动决策的核心。4.5 状态更新与主循环主循环负责调度根据决策执行工具然后把工具结果更新到状态中。伪代码如下def run_state_agent(task_id: str) - OrderTaskState: state OrderTaskState(task_idtask_id) max_steps 10 while not state.finished and state.step_count max_steps: state.step_count 1 decision llm_decision(state) action decision[action] if action query_order: result query_order(state, decision[params][order_id]) state.order_id result[order_id] state.user_id result[user_id] state.order_info result state.status queried elif action calculate_cost: state.total_cost calculate_cost(state) state.status calculated elif action send_notice: message send_notice(state) state.status notified print(message) elif action finish: state.finished True # 打印每一轮决策后的状态观察上下文长度变化 print(fStep {state.step_count}, action{action}) print(state.to_prompt()) print( * 40) return state if __name__ __main__: final_state run_state_agent(TASK_20250101_001) print(任务完成, final_state.finished)4.6 运行结果与说明运行上述代码会看到每一轮的状态文本长度基本相同不会随着执行步数增加而膨胀。三步执行完毕后Agent 完成订单查询、金额计算和通知发送。这个示例与“完整历史”模式的最大区别在于完整历史模式下第二轮输入会包含第一轮的工具返回 JSON第三轮会包含前两轮的 JSON上下文越来越长而在状态模式下第二轮输入只包含“订单号、用户 ID、订单信息”第三轮输入只包含“金额已计算完成”等状态信息长度基本恒定。实际开发时可以把llm_decision替换为真实 LLM 调用把query_order等替换为真实的 API 或数据库查询这个框架依然成立。5. 在主流 Agent 平台与框架中的落地参考5.1 对话式智能体平台中的状态节点现在很多团队会使用 Dify、Coze 等智能体平台快速搭建应用。这类平台通常支持“变量”、“状态节点”或“记忆管理”功能正好可以用来实现显式执行状态。以订单处理场景为例可以在平台中定义全局变量order_id、order_status、user_id、step。工作流每执行一步就把工具结果的关键字段写入变量而不是把完整工具输出留在对话上下文里。后续节点的 Prompt 只引用这些变量上下文长度就控制住了。需要注意不同平台对“会话变量”和“消息历史”的处理方式不同。在配置工作流时建议手动关闭“自动拼接待办历史”这类选项改为显式引用变量才能达到状态压缩的效果。5.2 代码型 Agent 框架中的显式状态在 LangGraph、AutoGen、LlamaIndex 等代码型框架中状态管理通常更灵活。例如 LangGraph 本身就支持定义 State Schema节点之间传递的是结构化状态而不是单纯的消息列表。落地时可以这样做把工具返回的原始结果写入节点的输出字段但不要直接加到消息列表。在状态 Schema 中定义execution_state字段专门存放关键变量。每个节点只从execution_state读取所需字段生成决策 Prompt。消息列表只保留用户与 Agent 的最终回复用于多轮对话展示。这样既保留了 LangGraph 的图编排能力又避免了工具输出不断堆积到上下文的问题。5.3 混合策略该保留的历史还是要保留需要特别强调的是SKILL.state 并不是要彻底取消对话历史。在多轮人机交互场景中用户的意图、偏好、历史询问仍然需要一定上下文才能理解。比如用户说“刚才那个订单”如果完全没有对话历史Agent 就不知道“刚才”指哪个订单。比较合理的混合策略是用户与 Agent 的对话历史保留最近若干轮或者定期压缩成摘要。工具执行记录不进上下文只作为日志存储。任务执行状态显式维护结构化状态作为决策的主要依据。长尾信息放入外部存储或向量库按需检索。换句话说对话上下文和任务执行上下文要分开管理用不同的策略处理。6. 常见误区与排查思路6.1 误区一状态对象越大越好有些开发者把“显式状态”理解成“把所有可能用到的信息都塞进状态”结果状态对象膨胀状态文本比原来的对话历史还长反而失去意义。状态设计应该遵循“最小足够”原则只保留当前任务后续决策可能用到的字段。不常用的大字段放在外部存储中需要时再通过工具查询。6.2 误区二完全抛弃对话历史在前面已经提到对话历史并非完全没有价值。用户的多轮语言表达、纠错、补充信息往往依赖历史才能理解。如果在纯对话场景中强行用状态替代所有历史会导致 Agent 失去上下文理解能力。更好的做法是把“任务执行历史”和“对话历史”分开前者用状态管理后者用摘要或窗口管理。6.3 误区三状态更新出错后无法恢复状态是可变的这意味着一旦某个字段被错误更新后续决策可能全错。实际工程中这个问题比对话历史模式下更严重因为历史模式下模型还能从旧记录里发现矛盾。排查思路为状态变更增加日志记录每个字段的旧值和新值。增加状态校验节点在关键步骤前后检查状态是否符合预期。为状态增加版本号必要时支持回滚。6.4 常见问题速查表问题现象常见原因解决思路状态文本太长上下文没降下来状态字段设计过大塞入了大量原始数据精简字段长文本改存外部按需查询Agent 失去用户早期意图完全抛弃对话历史保留最近 N 轮对话或维护用户意图摘要某一步状态更新错误导致后续全错缺少状态校验与回滚机制增加版本号、校验逻辑、错误分支决策模块频繁要求查询工具状态中缺少必要字段在工具结果解析阶段提取关键字段写入状态状态与真实业务数据不一致业务系统数据变更后状态未同步在关键节点主动刷新状态不依赖缓存7. 工程实践建议7.1 状态建模像设计数据库表一样设计状态显式状态是智能体的“数据库”设计时可以参考数据库表设计原则字段原子化、避免冗余、明确主键、设置合理的类型。例如订单任务状态中订单金额应该是一个数值字段而不是一段描述文字。这样后续计算、比较、校验都能直接使用而不需要让模型从文本中解析。7.2 工具结果解析后再入状态一个常见的错误是工具返回 JSON 后直接整个塞进状态。这仍然会造成状态膨胀。正确的做法是raw_result query_order_api(order_id) state.order_id raw_result[order_id] state.user_id raw_result[user_id] state.total_cost float(raw_result[amount]) state.order_info { status: raw_result[status], items_count: len(raw_result[items]), }只提取决策需要的字段。原始结果如果需要留档写入日志系统或数据库而不是塞进上下文。7.3 安全边界状态中不要放敏感明文状态文本最终会作为 Prompt 的一部分发送给 LLM。如果状态中包含密码、密钥、身份证号等敏感信息这些信息会暴露给模型并可能出现在日志中。建议状态中只保存脱敏后的信息如user_idU_10086而不是完整手机号。涉及扣款、删除、退款等高风险操作时必须有明确的用户授权确认节点。状态变更操作遵循最小权限原则只有 Agent 的必要工具才能修改对应字段。7.4 可观测性状态快照是最重要的调试信息因为模型是基于状态决策的排查问题时第一件事就是看“当时的状态是什么”。因此建议在每轮决策前保存一份状态快照连同决策动作、工具结果一起写入日志。这样当 Agent 出现误判时可以快速定位是状态缺失导致模型无法判断还是状态错误导致模型判断错误还是模型本身能力不足。7.5 性能与成本控制采用显式执行状态后上下文长度趋于稳定但仍然要控制状态文本的 token 预算给状态序列化文本设置最大长度超长时启用精简模式。状态字段按访问频率排序高频字段放在 Prompt 前半部分。对关键字段使用紧凑格式例如paid而不是payment_statuspaid。8. 总结与下一步学习方向SKILL.state 的价值在于把智能体的上下文管理问题从“如何压缩越来越长的历史”转变成“如何设计一份好的执行状态”。后者是一个工程问题可以通过结构化建模、校验、日志和版本控制来系统性地解决。对于正在做 Agent 开发的团队我的建议是先在日志里统计一下一个长任务跑完后真正被模型用到的历史 token 占总 token 的比例是多少。你可能会发现大量 token 花在了从未被有效利用的工具输出和中间推理上。从这个数据出发再决定是否采用显式状态方案会更有说服力。如果你对智能体上下文优化感兴趣下一步可以重点研究这几个方向LangGraph 的状态图设计、会话摘要与长期记忆、工具结果的结构化解析、多 Agent 协作时的状态共享与隔离。这些话题都和 SKILL.state 一脉相承核心都是同一个问题如何让智能体在有限的上下文里做出更准确的决策。
返回列表