ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AI Agent上下文管理:用SKILL.state的思路替代对话历史

AI Agent上下文管理:用SKILL.state的思路替代对话历史 之前不少团队在做 AI Agent 落地时都会遇到同一个问题模型明明记住了上一轮对话但换一个会话、重启一次服务或者任务稍微长一点上下文就开始“漂移”。大家习惯性把对话历史一股脑塞给模型以为这就是“记忆”结果 token 越塞越多回答质量反而下降。Google 最近提出的 SKILL.state 方向就是把这个问题重新拆了一遍与其无限保留对话历史不如在关键节点上维护一份显式状态。这篇文章会结合 SKILL.state 的研究思路讲清楚显式状态与对话历史的区别并给出一套可直接参考的代码级实现方案帮助你在实际开发中理解“如何保存对话上下文”这件事。1. SKILL.state 是什么解决什么问题1.1 从一组对话记录说起先看一个最常见的场景。用户让 Agent “帮我把项目里的 Java 版本从 8 升级到 17”Agent 先扫描了项目结构然后修改了 pom.xml接着处理了几个依赖冲突最后跑测试并给出报告。如果把这个过程完整记录下来你会得到一份非常长的对话历史用户帮我升级项目 Java 版本 Agent好的我先看一下项目结构 用户项目在 /data/project Agent扫描到 pom.xml当前版本是 1.8 用户改用 Java 17 Agent正在修改 pom.xml ...这段记录里真正有长期价值的并不是每一句话而是以下几个关键信息目标项目路径是 /data/project构建工具是 Maven当前 Java 目标版本是 17需要修改的文件是 pom.xml已经完成的步骤有依赖冲突处理下一步要执行测试命令。这些信息合在一起就是 Agent 在执行这个任务时的“状态”。对话历史只是状态的一种外在表现形式它把真正有用的变量和大量冗余文本混在一起。1.2 对话历史作为“隐式状态”的瓶颈大多数 AI Agent 框架默认用“消息列表”来保存上下文。这种方案在短对话里没有问题但在长任务、多轮工具调用、跨会话恢复场景里会暴露三个明显瓶颈。第一是 token 膨胀。每一轮工具调用返回的 JSON、代码片段、日志都可能很长模型每次请求都要重新读一遍完整的历史。任务越复杂历史越长推理成本和延迟都会快速上升。第二是信息噪声。对话历史里包含大量“过程性描述”比如“我正在读取文件”“这一步执行成功”。这些内容对于理解当前任务并不是每次都有用但模型必须处理它们这会稀释真正重要的状态信息。第三是恢复困难。假设进程崩溃了或者用户想从上一个断点继续任务只保存对话历史往往不够。模型需要从几百条消息里重新推断“现在做到哪一步了”推断错了后续步骤就会出错。换句话说对话历史是一种隐式状态。它把状态“藏”在文本里让模型自己理解和还原。SKILL.state 的研究思路正是要把这份状态显式地抽出来单独维护。1.3 SKILL.state 的核心主张显式状态替代隐式历史SKILL.state 可以理解为“面向技能执行的状态管理设计”。它的核心主张是Agent 在执行任务时不应该只保存“聊了什么”而应该保存“任务做到哪一步、当前掌握了哪些事实、下一步该做什么”。从工程视角看显式状态有点类似于传统后端开发中的“领域模型”。后端系统不会把用户的所有请求日志当作业务数据而是会把核心信息更新到数据库对应字段里。Agent 的长期上下文也应该遵循类似的思路。和对话历史的对比可以用下面这张表来快速理解维度对话历史隐式状态SKILL.state显式状态表达形式自然语言消息列表结构化字段、对象、键值对信息密度低包含大量过程性内容高只保留关键变量token 消耗随时间线性增长基本保持稳定跨会话恢复需要模型重新理解直接加载并继续执行更新方式追加一条新消息增量修改状态字段出错风险长文本中信息易丢失字段明确可校验、可追踪这种设计的好处是模型不需要在每次请求时“重新读一遍小说”只需要读一份结构清晰的“项目进度表”。2. 工作原理拆解从隐式上下文到结构化状态2.1 状态、动作与技能的分工要理解 SKILL.state可以先把它放到一个大的 AI Agent 体系里看。一个完整技能执行流程通常包含三个层次技能Skill一类可复用能力的定义例如“升级 Java 版本”“部署应用到服务器”。动作Action技能执行过程中的具体步骤例如“读取 pom.xml”“执行 mvn test”。状态State记录当前执行环境和已经发生事实的结构化数据。状态处于整个流程的中心。动作执行前后Agent 都要更新状态每一次大模型决策时都会先读取状态再决定下一步动作。这样的设计也方便 Agent 在不同步骤之间共享信息。比如第一个动作读取了项目路径后面所有动作都可以从状态里拿到这个值不需要每次都回到对话历史里搜索。2.2 显式状态的持久化方式显式状态不能只存在于程序内存里否则进程一结束就丢失了。研究思路里强调的持久化通常指的是把状态序列化成一种可保存、可加载、可共享的格式。常见的表现形态有JSON 文件适合单机工具可直接人工检查和修改数据库记录适合服务化 Agent支持多端共享和并发控制对象存储适合包含大文件的场景状态中只保存文件 URL版本化目录适合复杂任务每一步操作后都生成新的状态快照。在下面的实战代码中我会用 JSON 文件作为持久化载体因为它最直观适合理解思路。真正落到生产环境再根据规模替换成数据库或对象存储即可。2.3 与 RAG、向量记忆机制的区别很多读者会问SKILL.state 和目前常见的“向量记忆”“RAG 知识库”有什么区别RAG检索增强生成主要负责从外部知识库中检索信息解决的是“模型不知道某件事”的问题。比如 Agent 要去查询公司内部的部署规范会先向量化这些规范文档用户提问时先检索相关内容再丢给模型。向量记忆解决的是“相似的历史怎么找回”的问题。例如用户说“还是按上次那个风格改”系统会把这句话转成向量到历史记录里查找最相似的记录。而 SKILL.state 解决的是“当前任务进行到哪一步”的问题。它不关心历史上说过多少句话也不关心外部文档有多长它只关心执行流程中必须持续跟踪的事实数据。两者可以同时存在用 RAG 补充领域知识用显式状态维护任务进度。3. 映射到工程场景Claude Code / AI Agent 里的状态保存近期“claude code 怎么保存对话历史”这类问题热度很高本质上也是开发者在为“对话状态丢失”感到困扰。我们需要把 SKILL.state 的思路落到这些实际 Agent 场景中才能知道它到底怎么用。3.1 为什么“保存对话历史”不等于“恢复上下文”很多 AI 编程工具都提供了“导出对话记录”功能把命令行里的所有消息保存成 Markdown 或 JSON。但导出对话历史只是把聊天记录落盘并没有把任务状态恢复出来。举例来说假设之前用 Claude Code 完成了一个“给项目添加 Redis 缓存”的任务。导出文件里会包含用户输入的若干条指令Agent 读取过的文件路径执行过哪些重构命令最后输出的一段总结。如果你把这个文件重新交给一个新的 Agent 会话让模型“根据这个对话继续”它大概率会从零开始重新理解任务。真正高效率的做法是直接告诉新会话当前项目状态已添加 spring-boot-starter-data-redis 依赖 已完成RedisConfig 配置类 已完成UserService 缓存注解 待办处理缓存穿透问题 目标保证查询性能优化后不影响一致性这一份描述才是真正可恢复的显式状态。它会比几百条对话消息更精炼、更准确。3.2 什么才算“好的显式状态”判断一份状态设计得好不好可以从四个角度评估。第一最小化。状态里只放任务执行必需的变量不把无关的历史细节都塞进去。例如“刚才用户在第 5 行代码里打了感叹号”这种信息如果没有后续影响就不该出现在状态里。第二可解释。每个状态字段都应该能被业务人员看懂。target_java_version: 17比history[3].content.substring(0, 20)更容易理解。第三可校验。状态里的字段应该有类型和取值范围约束不能只是自由文本。否则 Agent 从状态里读出的值可能本身就是脏的。第四可回溯。应该能记录“状态什么时候被谁更新成了什么”而不是只保存一个当前值。这样即使任务执行失败也可以回到上一个可用状态。3.3 状态保存的一个场景示范假设你在用 AI 编程工具做一次“JDK 8 升级到 JDK 17”的改造过程可能跨越好几天中途模型需要反复重启、切换分支、甚至在另一台电脑上继续。如果你用的是对话历史所有关键事实都散落在上千行终端日志中如果你用 SKILL.state 的思路维护了一个状态文件文件内容可能是这样的{ task: java_upgrade_17, updated_at: 2025-06-10T18:30:0008:00, variables: { project_path: /data/my-service, maven_module: [api, core, infra], source_level: 8, target_level: 17 }, pending: [处理 MapStruct 与 Java 17 的兼容问题], done: [修改父 pom 的 java.version 属性, 排除重复依赖 junit] }这份状态文件可以跟随 Git 提交也可以放入对象存储。下一次继续任务时只需要把这份文件中的pending和variables交给模型它就能快速接上进度。4. 代码实战实现 SKILL.state 思路的 Agent 会话管理器为了让上面的概念落地这一节用一个 Python 示例演示如何实现一个带显式状态的 Agent 会话管理器。这个示例不需要连接任何大模型 API核心是展示状态如何定义、如何更新、如何恢复。4.1 使用场景设定假设要设计一个“代码迁移助手”它的任务是帮助用户升级项目中的 Java 版本。传统方案会保存一个 messages.json把我们和模型的全部对话存下来。现在我们只维护一个结构化状态对象。先定义状态的数据结构。为了让示例简单我直接使用 Python 字典和 dataclass 来管理并用 JSON 文件持久化。# 文件路径agent_state/state.py from dataclasses import dataclass, field, asdict from typing import Dict, List from datetime import datetime import json dataclass class TaskState: task_name: str updated_at: str variables: Dict[str, object] field(default_factorydict) done: List[str] field(default_factorylist) pending: List[str] field(default_factorylist) def __post_init__(self): if not self.updated_at: self.updated_at datetime.now().isoformat(timespecseconds) def mark_done(self, step: str): 把某个待办移入已完成列表并更新时间戳。 if step in self.pending: self.pending.remove(step) if step not in self.done: self.done.append(step) self.updated_at datetime.now().isoformat(timespecseconds) def add_pending(self, step: str): 新增一个待办事项避免重复添加。 if step not in self.pending and step not in self.done: self.pending.append(step) self.updated_at datetime.now().isoformat(timespecseconds)这段代码定义了三个核心能力variables保存项目环境中的关键变量done记录已经完成的操作pending记录待执行的步骤。这里的mark_done方法模拟了 Agent 每完成一步动作后的“状态更新”行为。4.2 持久化保存与恢复状态有了状态结构之后下一步是提供两个工具函数一个负责把状态写到硬盘另一个负责从硬盘读取状态。这是整个显式状态管理最核心的基础设施。# 文件路径agent_state/store.py import json import os from pathlib import Path from typing import Optional from .state import TaskState def save_task_state(state: TaskState, path: str) - str: 将状态对象序列化为 JSON 并写入文件。 为了安全先写入临时文件再替换旧文件 避免进程中断时留下半截坏数据。 save_path Path(path) save_path.parent.mkdir(parentsTrue, exist_okTrue) tmp_path save_path.with_suffix(suffix.tmp) payload json.dumps( { task_name: state.task_name, updated_at: state.updated_at, variables: state.variables, done: state.done, pending: state.pending, }, ensure_asciiFalse, indent2, ) tmp_path.write_text(payload, encodingutf-8) os.replace(tmp_path, save_path) return fstate saved - {save_path} def load_task_state(path: str) - Optional[TaskState]: 从 JSON 文件恢复状态如果文件不存在则返回 None。 load_path Path(path) if not load_path.exists(): return None with open(load_path, r, encodingutf-8) as f: data json.load(f) return TaskState( task_namedata.get(task_name, unknown_task), updated_atdata.get(updated_at, ), variablesdata.get(variables, {}), donedata.get(done, []), pendingdata.get(pending, []), )存储部分有两个值得注意的细节。一个是写临时文件后os.replace这个操作在大部分操作系统上是原子的可以防止进程中途崩溃导致 JSON 文件损坏。另一个是ensure_asciiFalse这样保存的中文内容在文件里是可读的。生产环境排查问题时可以直接打开状态文件人工核对。4.3 把状态转换成模型可读的 Prompt显式状态的价值在于能被大模型直接使用。所以还需要一个方法把状态对象压缩成一段结构化的系统提示词而不需要用对话历史填充。# 文件路径agent_state/prompt_builder.py from .state import TaskState def build_state_prompt(state: TaskState) - str: 将当前状态渲染为模型输入的一段 system prompt。 这段文本的核心思想只保留必要信息减少历史噪声。 variables_block \n.join( f- {key}: {value} for key, value in state.variables.items() ) done_block \n.join(f- {item} for item in state.done) or - 暂无 pending_block \n.join(f- {item} for item in state.pending) or - 暂无 return f当前任务{state.task_name} 状态更新时间{state.updated_at} 关键变量 {variables_block} 已完成步骤 {done_block} 待执行步骤 {pending_block} 请根据上述状态继续执行不要重复已完成步骤。 对比一下传统对话历史的 Prompt 可能是几万字的消息列表而这里生成的 Prompt是一份结构清晰的“项目卡片”。模型读完这份卡片后能准确知道当前进度不会把已经完成的事情再做一遍。4.4 完整流程演示下面把整个流程串起来演示“开始任务 → 更新状态 → 中断 → 恢复任务”的完整循环。# 文件路径examples/demo_skill_state.py import sys from pathlib import Path # 将项目根目录加入模块搜索路径方便直接从命令行运行 sys.path.append(str(Path(__file__).resolve().parents[1])) from agent_state.state import TaskState from agent_state.store import save_task_state, load_task_state from agent_state.prompt_builder import build_state_prompt STATE_FILE examples/tmp_task_state.json def main(): # 第一次新任务初始化状态 state load_task_state(STATE_FILE) if state is None: print( 没有历史状态开始新任务初始化) state TaskState( task_namejava_version_upgrade, variables{ project_path: /data/my-service, build_tool: maven, source_version: 8, target_version: 17, }, done[], pending[ 扫描项目模块结构, 修改父 pom 的 java.version 属性, 检查依赖冲突, 运行单元测试, ], ) else: print( 检测到历史状态直接恢复任务进度) # 模拟 Agent 第二步动作扫描模块结构完成 state.mark_done(扫描项目模块结构) # 模拟新增一个后续待办例如模型发现 MapStruct 版本过旧 state.add_pending(升级 MapStruct 到兼容 Java 17 的版本) # 更新 state.variables 中的关键信息 state.variables[module_count] 3 # 保存状态 save_task_state(state, STATE_FILE) # 输出 Prompt print(\n 模型可读的显式状态提示词 \n) print(build_state_prompt(state)) if __name__ __main__: main()运行这个脚本前先确保目录结构如下skill-state-demo/ ├── agent_state/ │ ├── __init__.py │ ├── state.py │ ├── store.py │ └── prompt_builder.py └── examples/ ├── demo_skill_state.py └── tmp_task_state.json使用命令行执行cd skill-state-demo python examples/demo_skill_state.py第一次执行的输出大致如下 没有历史状态开始新任务初始化 状态已保存 模型可读的显式状态提示词 当前任务java_version_upgrade 状态更新时间2025-06-11T10:15:3208:00 关键变量 - project_path: /data/my-service - build_tool: maven - source_version: 8 - target_version: 17 - module_count: 3 已完成步骤 - 扫描项目模块结构 待执行步骤 - 修改父 pom 的 java.version 属性 - 检查依赖冲突 - 运行单元测试 - 升级 MapStruct 到兼容 Java 17 的版本 请根据上述状态继续执行不要重复已完成步骤。再次运行同一段脚本输出会变为 检测到历史状态直接恢复任务进度状态文件被加载后任务继续推进不会因为进程重启而丢失。4.5 和“保存对话历史”方案对比作为收尾我们做一次小规模对比从三个维度看看显式状态方案的优势。对比项目保存对话历史SKILL.state 显式状态磁盘占用持续增长可能达到 MB 级稳定在 KB 级恢复后能否直接执行模型需要重读并推断上下文字段明确可以直接继续是否能回答“我现在在哪一步”需要人工翻阅打开 JSON 即可看到 pending 列表这里并不是说对话历史完全没有作用。对话历史仍然适合做审计追踪、用户偏好分析和异常定位但在“让 Agent 接着干活”这件事上它不应该替代显式状态的位置。5. 常见问题与排查思路5.1 状态文件读取报错错误现象json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes常见原因状态文件在写入过程中被中断残留了不完整 JSON或者手写文件时少了逗号、少了引号。解决思路# 先备份损坏文件 cp task_state.json task_state.json.bak # 用 python 工具检测语法位置 python -m json.tool task_state.json如果损坏文件无法恢复可以回到 Git 历史或对象存储的上一个版本。这也是 4.2 节使用“先写临时文件再替换”的原因就是为了减少这类风险。5.2 任务恢复后出现了重复操作错误现象Agent 恢复后把之前已经改过的 pom.xml 又改了一遍。常见原因状态中done列表不完整模型不知道某些步骤已经完成或者不同的动作描述实际指向同一个操作。解决思路在mark_done时使用精确的操作描述不要写“处理依赖问题”这种模糊文本把关键文件路径放入variables让模型判断是否已处理每次更新状态后检查done和pending是否有重复交集。5.3 状态字段不断膨胀错误现象状态里的pending越加越多从 5 项涨到 50 项失去可读性。常见原因代码把模型的每一步输出都当成新 pending而不是把多个子步骤合并成一个待办。排查思路问题现象常见原因解决思路pending 越来越多每个小动作都单独记录使用层级式待办例如“模块 A 升级”下挂多个子项done 有大量相似项步骤命名不够统一定义动作规范词表例如“修改配置、运行测试、提交代码”状态恢复后行为不一致状态字段类型不固定增加 schema 校验确保 value 类型稳定多端同时修改状态没有版本控制引入 version 字段保存时判断是否为最新值5.4 状态文件被并发写入在多进程或多线程场景下两个 Agent 同时更新一个状态文件会导致更新丢失。解决思路是加入版本号类似乐观锁# 在 TaskState 中增加 version 字段 version: int 1 def mark_done(self, step: str): if self.version 0: raise ValueError(状态版本异常请重新加载) # ... 更新逻辑 self.version 1保存时带上版本号如果文件中的版本号比内存中的新说明有其他端更新过状态应该先重新加载再合并。6. 最佳实践与工程建议6.1 状态字段要有 schema 校验显式状态本质上是一份有约束的数据不能像对话历史一样“想到哪写到哪”。建议团队在状态模块中引入 schema 定义例如用 JSON Schema 约束必填字段和类型。这样即使 Agent 给自己“编造”了一个错误字段保存阶段也能及时发现而不是带着脏数据继续往后跑。6.2 区分长期状态与临时状态不是所有信息都值得放进 SKILL.state。建议按以下规则区分长期状态任务目标、项目路径、关键配置值、已完成步骤、当前待办。临时变量某次函数调用的中间结果、模型生成的临时总结、用户可以随时再说一遍的话。长期状态写入持久化文件临时变量只保存在当前进程里。避免把 Agent 执行过程中的每一条输出都变成永久状态。6.3 每次执行动作后都更新状态很多 Agent 框架只有在任务结束时才保存对话记录中间不保存任何东西。这会导致一个很尴尬的情况任务执行 20 分钟后崩溃所有进度全部丢失。更合理的做法是每完成一个原子动作就更新一次状态。这里的“原子动作”可以是成功修改一个文件成功执行一条命令成功调用一次检索 API成功完成一次用户确认。更新频率提高会增加少量 I/O 成本但换来的是高韧性的任务恢复能力在生产环境中非常划算。6.4 让大模型来维护状态结构有了结构化状态和 Prompt 之后可以进一步让模型自己维护状态。例如每轮交互结束时系统给模型发一个工具调用请求update_task_state让模型从对话里抽取新的变量并调用本地函数来更新状态对象。示例伪代码如下def update_task_state(task_state, user_message, model_reply): 让模型分析用户输入和模型输出返回结构化的状态增量。 生产环境建议使用对应的 function calling 能力这里只描述接口约定。 extracted llm_extract( contentuser_message model_reply, schema{ done: [string], pending: [string], variables: {type: object}, } ) for step in extracted.get(done, []): task_state.mark_done(step) for step in extracted.get(pending, []): task_state.add_pending(step) if extracted.get(variables): task_state.variables.update(extracted[variables])这个思路本质上是把“从对话历史维护状态”的工作自动化而不是让开发者每次手工定义一个mark_done。6.5 状态文件应该进入版本管理对于代码项目类 Agent 任务强烈建议把状态文件提交到 Git 仓库。这样可以查看每个时间点的任务状态变化任务出错时可以通过git diff精确定位是哪一步改变了关键变量可以支持“回滚到上一个版本”的任务恢复策略。当然状态文件里如果包含密钥、内部 IP 等敏感信息就不要直接进仓库应使用环境变量或密钥管理服务。6.6 注意保留对话历史用于审计虽然状态替代了大部分历史但对话历史仍有价值。发生问题时我们需要回溯“用户当时到底说了什么Agent 才会做出错误修改”。状态解决的是执行问题历史解决的是责任归属和过程审计问题。两者不是二选一的关系。7. 总结与后续学习方向围绕 SKILL.state 的讨论核心是对“上下文的本质”做了一次重新审视对话历史不是唯一值得保存的东西甚至不是最高效的记忆载体。真正能让 Agent 稳定工作、跨会话恢复的是一份不断更新的显式状态。在实际开发中你可以从今天这篇代码示例出发把 Agent 的消息记录与状态对象拆开先给状态加上 JSON 持久化再逐步把变量更新、待办管理、版本控制纳入体系。接下去可以继续学习的方向包括函数调用function calling与工具调用的状态同步机制更复杂的任务编排框架中状态机的建模方式使用向量数据库保存长期用户偏好与短期执行状态互补Agent 任务的可观测性设计如何记录状态变更事件方便后续追踪。核心要记住一个原则不要往大模型的上下文里堆一切内容要为每类信息找到精准的容器。任务进度放进显式状态领域知识交给检索系统过程记录保留在日志中这样模型才能轻装上阵任务也更容易稳定落地。
返回列表