SKILL.md 技能文档
能力目标
把默认「几乎永远够不着」的压缩阈值调成你自己的值,让长对话在可控的点位上被压缩;能确认压缩真的发生过(而不是凭感觉)、读到生成的摘要、算清这次压缩的真实开销,并知道截断与摘要的执行先后。
前置
已能构造智能体(见 bootstrapping-deepagents-env)。
一个先决事实:压缩不是你开启的,装具入口默认就挂了摘要中间件(主智能体与通用子智能体各一件),只是默认触发阈值高达十几万 token,短对话永远触达不到。你要做的不是「加上它」,而是把阈值调小并用自己的实例覆盖默认那件。
验证安装:
python -c "import deepagents; from deepagents.middleware.summarization import SummarizationMiddleware; print('ok')"
实操流程
写一个可复用的构造函数,把摘要中间件配好并覆盖默认实例:
from deepagents import create_deep_agent from deepagents.middleware.summarization import SummarizationMiddleware from deepagents.backends import FilesystemBackend def make_agent_with_summarization(workspace_path, trigger_tokens=800, keep_messages=4): model = get_model() backend = FilesystemBackend(root_dir=workspace_path, virtual_mode=True) mw = SummarizationMiddleware( model=model, backend=backend, trigger=("tokens", trigger_tokens), # 触发阈值:单位 + 数值 keep=("messages", keep_messages), # 压缩后保留最近几条原始消息 truncate_args_settings={ "trigger": ("messages", 100), "keep": ("messages", 20), }, ) agent = create_deep_agent( model=model, middleware=[mw], # 用自定义实例覆盖默认高阈值中间件 system_prompt="你是一个有记忆的助理,需要记录并能随时调用之前对话中的术语。", ) return agent, model, backend, mw触发阈值与保留条数都是「单位 + 数值」二元组,单位可取 token 数或消息条数;后端决定压缩前的原文落盘位置,给无损找回留后路。
定阈值前先认清计数口径。框架内部用快速的字符比例估算,与厂商实际计费的 token 数有差距,中文尤其被低估。实测同一份内容近似估算 675、厂商实报输入 1261、实报总计 1341,差约一半。结论:别把近似值当精确触发点,给阈值留足余量、并接受触发点会有抖动。
喂足够长的对话真触发一次压缩,然后确认它真的发生了。判据是压缩事件对象从空变成一个含三字段的记录:在第几条消息处切开、生成的摘要消息、原文落盘路径。压缩事件是私有状态字段,不会出现在调用返回的状态里,所以有两条观测路径:
- 生产代码里:检查返回消息数是否骤降、或去文件系统里找那个落盘文件。
- 要看内部值时:按「判断是否该压缩 → 定切点 → 生成摘要」的顺序直接调中间件的内部方法跑一次,把三个字段打出来。
读一遍生成的摘要,看清它是什么。摘要有固定的四节结构:会话意图、摘要正文、产物(其中嵌入原文落盘路径)、后续步骤。压缩是模型做的抽取式改写、不是截断删除——这也正是信息会丢的根源,默认摘要提示词明确要求「只保留最相关的信息」。
算这次压缩的账。每次压缩都是一次额外的真实模型调用,可以给摘要模型单独挂回调统计用量:
from langchain_core.callbacks import BaseCallbackHandler class LLMCallTracker(BaseCallbackHandler): def on_llm_end(self, response, **kwargs): usage = response.llm_output.get("token_usage", {}) self.total_input_tokens += usage.get("prompt_tokens", 0) self.total_output_tokens += usage.get("completion_tokens", 0) summary_model = ChatOpenAI(model="deepseek-chat", base_url="...", api_key=..., callbacks=[LLMCallTracker("summary-model")]) mw = SummarizationMiddleware(model=summary_model, backend=backend, trigger=("tokens", 800), keep=("messages", 4))换一个更小更便宜的摘要模型,就是换这个
model参数。需要更轻量的省法时,单独用工具参数截断。它只动写文件与编辑文件两个工具的超长字符串参数、不调模型,且有自己独立的、比摘要更低的阈值:
mw = SummarizationMiddleware( model=model, backend=backend, trigger=("tokens", 9999), # 摘要阈值设高、本轮不触发摘要 truncate_args_settings={ "trigger": ("messages", 2), "keep": ("messages", 2), "max_length": 2000, # 超过这个字符数的字符串参数才截 "truncation_text": "...(argument truncated)", }, )想让模型自己决定何时压缩(比如任务切换时),就用摘要工具中间件。它不直接接模型与后端,必须先建一个摘要中间件再包进去,复用同一套摘要引擎与同一个压缩事件状态:
SummarizationToolMiddleware(summarization=summ_mw)
校验回路
- 压缩确认三件套:压缩事件非空且带切点下标、消息数从压缩前的规模骤降到「1 条摘要 + 保留的若干条」、落盘目录下出现对应的会话原文文件。任一为空就是没触发,加长对话或继续降阈值,直到它非空。
- 压缩比:用真实 token 数算压缩前后的比值,实测一次为 3.70 倍(2604 降到 703、消息 40 条降到 5 条)。这个数可以反过来指导你调阈值。
- 截断生效的判据:超长参数被压成「前若干字符 + 截断标记」,而工具名、调用 ID、消息结构原样保留;同时非目标工具的大参数不受影响。
- 主动压缩工具的接口确认:中间件建好后其工具列表含一个名为
compact_conversation的工具。
常见陷阱
- 压缩死活不触发:对话太短或阈值太高。加长对话、降阈值,并记住近似计数会低估、真实触发点比预设更早。
- 后端没开虚拟模式:原文落盘会试图写到系统根路径、报只读文件系统错误。直接用磁盘后端并开启虚拟模式即可,不要再套一层组合后端包装,否则路径会双重叠加。
- 保留条数与配置值对不上:这是正常现象。切点若正好落在一条工具消息上,框架会回溯找到它配对的那条模型消息,宁可多压一点也不留下孤立的工具结果——因为厂商接口会拒收找不到对应调用的工具结果。
- 以为三条路径是三个并列开关:每次模型调用走的是固定流水线:先做最轻量的参数截断,截断后再判断要不要摘要,最后执行调用;若仍然超窗则兜底触发一次摘要再重试。所以光靠截断就省够了的那一轮,会直接跳过摘要。
- 只看单次压缩的账就断言亏了:实测单次压缩名义省下 1345 token,而摘要本身花了 5670 token,单次净亏。收益在之后每一轮:历史缩短后每轮请求都按更短的历史发送、省下的量随轮数累加。判断要不要压、压多频,本质是看剩余对话还有多长。
- 直接调用主动压缩工具:脱离智能体运行时上下文调用会报运行时字段缺失的校验错误,必须在完整会话内由模型调用。它还有一道资格门:实报用量达到自动触发阈值的一半才允许压缩,防止在空会话上瞎压。
- 把它和大工具结果卸载混为一谈:摘要是把旧对话历史交给模型凝练、有损;卸载是把单条大工具结果整段搬进文件、无损可回读。两者的执行顺序是卸载先行、摘要兜底。
适用范围与前置条件
已能构造智能体(见 bootstrapping-deepagents-env)。
一个先决事实:压缩不是你开启的,装具入口默认就挂了摘要中间件(主智能体与通用子智能体各一件),只是默认触发阈值高达十几万 token,短对话永远触达不到。你要做的不是「加上它」,而是把阈值调小并用自己的实例覆盖默认那件。
验证安装:
python -c "import deepagents; from deepagents.middleware.summarization import SummarizationMiddleware; print('ok')"
怎么使用
使用步骤
写一个可复用的构造函数,把摘要中间件配好并覆盖默认实例:
from deepagents import create_deep_agent from deepagents.middleware.summarization import SummarizationMiddleware from deepagents.backends import FilesystemBackend def make_agent_with_summarization(workspace_path, trigger_tokens=800, keep_messages=4): model = get_model() backend = FilesystemBackend(root_dir=workspace_path, virtual_mode=True) mw = SummarizationMiddleware( model=model, backend=backend, trigger=("tokens", trigger_tokens), # 触发阈值:单位 + 数值 keep=("messages", keep_messages), # 压缩后保留最近几条原始消息 truncate_args_settings={ "trigger": ("messages", 100), "keep": ("messages", 20), }, ) agent = create_deep_agent( model=model, middleware=[mw], # 用自定义实例覆盖默认高阈值中间件 system_prompt="你是一个有记忆的助理,需要记录并能随时调用之前对话中的术语。", ) return agent, model, backend, mw触发阈值与保留条数都是「单位 + 数值」二元组,单位可取 token 数或消息条数;后端决定压缩前的原文落盘位置,给无损找回留后路。
定阈值前先认清计数口径。框架内部用快速的字符比例估算,与厂商实际计费的 token 数有差距,中文尤其被低估。实测同一份内容近似估算 675、厂商实报输入 1261、实报总计 1341,差约一半。结论:别把近似值当精确触发点,给阈值留足余量、并接受触发点会有抖动。
喂足够长的对话真触发一次压缩,然后确认它真的发生了。判据是压缩事件对象从空变成一个含三字段的记录:在第几条消息处切开、生成的摘要消息、原文落盘路径。压缩事件是私有状态字段,不会出现在调用返回的状态里,所以有两条观测路径:
- 生产代码里:检查返回消息数是否骤降、或去文件系统里找那个落盘文件。
- 要看内部值时:按「判断是否该压缩 → 定切点 → 生成摘要」的顺序直接调中间件的内部方法跑一次,把三个字段打出来。
读一遍生成的摘要,看清它是什么。摘要有固定的四节结构:会话意图、摘要正文、产物(其中嵌入原文落盘路径)、后续步骤。压缩是模型做的抽取式改写、不是截断删除——这也正是信息会丢的根源,默认摘要提示词明确要求「只保留最相关的信息」。
算这次压缩的账。每次压缩都是一次额外的真实模型调用,可以给摘要模型单独挂回调统计用量:
from langchain_core.callbacks import BaseCallbackHandler class LLMCallTracker(BaseCallbackHandler): def on_llm_end(self, response, **kwargs): usage = response.llm_output.get("token_usage", {}) self.total_input_tokens += usage.get("prompt_tokens", 0) self.total_output_tokens += usage.get("completion_tokens", 0) summary_model = ChatOpenAI(model="deepseek-chat", base_url="...", api_key=..., callbacks=[LLMCallTracker("summary-model")]) mw = SummarizationMiddleware(model=summary_model, backend=backend, trigger=("tokens", 800), keep=("messages", 4))换一个更小更便宜的摘要模型,就是换这个
model参数。需要更轻量的省法时,单独用工具参数截断。它只动写文件与编辑文件两个工具的超长字符串参数、不调模型,且有自己独立的、比摘要更低的阈值:
mw = SummarizationMiddleware( model=model, backend=backend, trigger=("tokens", 9999), # 摘要阈值设高、本轮不触发摘要 truncate_args_settings={ "trigger": ("messages", 2), "keep": ("messages", 2), "max_length": 2000, # 超过这个字符数的字符串参数才截 "truncation_text": "...(argument truncated)", }, )想让模型自己决定何时压缩(比如任务切换时),就用摘要工具中间件。它不直接接模型与后端,必须先建一个摘要中间件再包进去,复用同一套摘要引擎与同一个压缩事件状态:
SummarizationToolMiddleware(summarization=summ_mw)