会话与自动压缩¶
消息结构¶
Conversation.messages 保存的是类型化消息对象(定义在 src/xun/conversation_message.py):每个类自己负责生成 OpenAI 请求参数(completion_param())与落盘格式(to_json()),message_from_json() 是反化工厂。
| 类型 | 角色 | 承载内容 |
|---|---|---|
SystemPrompt |
system |
提示词正文、is_compressed(正文是否为摘要)、persist_sections(压缩后仍要保留的段落,AGENTS.md 就在这里) |
UserMessage |
user |
文本、已归一化的图片 URL(data 或 http(s))、可选的 response_schema |
RawOpenAIMessage |
assistant / tool |
原样保存的 OpenAI 参数字典(模型返回的消息与工具结果) |
| 方法 | 说明 |
|---|---|
completion_params() |
把整个会话展开成发给模型的 ChatCompletionMessageParam 列表;msg.role 给出角色 |
set_system_message_content(content, is_compressed=False) |
替换或插入第 0 条系统消息(agent.system() 的落点),保留已有段落 |
set_persistent_section(name, content) |
写入或删除一个持久段落(内容留空即删除);agent.sync_project_instructions() 用它注入 AGENTS.md |
add_user_message(content, images=None) |
追加用户消息;有图片时 content 变成 text/image_url 分块列表,PIL 图像转 data URL |
set_response_schema(schema) |
把 pydantic schema 的 JSON 附到末条用户消息上(结构化输出的注入点);末条不是用户消息则 ValueError |
add_agent_message(msg) |
写入 assistant 消息,自动剔除空的 tool_calls: [] |
add_tool_result(tool_call_id, content) |
写入 role="tool" 消息,内容取 content.value_str();序列化失败写成 [Error] Failed to serialize... |
pop_last_message_if_user() |
取消时回退最后一条用户消息 |
pop_from_last_user_message(inclusive=True) |
/retry(不含)与 /revise(含)的实现,返回被弹出的消息 |
compact_toolcall(keep_max=12) / compacted_toolcall_result(id) |
第一级回收,以及按 id 取回被回收的原文 |
compact(summarize, keep_recent) |
第二级摘要压缩;summarize(messages) 返回 None 表示放弃 |
clear() |
清空但保留系统消息,并重置 token、压缩计数与被压缩工具结果的原文 |
to_history(truncate=False) / content_to_text(content, truncate=False) |
界面用的 {role, content} 记录(truncate 截到 1000 字符) |
dumps() / dump(path) / loads() / load(path) |
序列化;落盘键 tokens_used 为历史兼容保留,消息按 kind(system / user / raw)区分 |
界面渲染不在这一层:/history 用 to_history(),/render 渲染的是显示层缓存的事件流,见 显示层与 Web 服务。
工具调用保存在 assistant 消息的 tool_calls 数组里(含参数字符串),工具结果单独作为 role="tool" 消息按 tool_call_id 关联。推理内容按 model.reasoning_field 自动探测(reasoning 或 reasoning_content)并原样回写,以便多轮保持。
旧存档读不进来
1.3.0 起的落盘格式给每条消息带上 kind 与 raw;更早版本直接存 OpenAI 参数字典,新代码载入会抛 KeyError: 'raw',只能由旧版本读取。
两种长度度量¶
| 度量 | 来源 | 用途 |
|---|---|---|
total_tokens |
模型返回的 usage.total_tokens |
自动压缩的唯一判据(压缩后重置为 None,避免立刻再触发) |
estimated_message_length() |
对 content / text / reasoning / reasoning_content 字段做字符数累加(递归进入嵌套 dict/list) |
估算回收比例,并据此决定「继续廉价回收」还是「升级为摘要」 |
分级压缩¶
flowchart TD
S["每轮调用前检查"] --> A["回收旧工具结果"]
A --> B{"仍超标?"}
B -->|否| E["本轮结束"]
B -->|是| C["摘要压缩"]
C --> D{"仍超标?"}
D -->|否| E
D -->|是| A
第一级:回收旧工具结果¶
超过 keep_max(默认 12)条的旧 role="tool" 消息被就地替换成占位符:
[Compacted, ID: <toolcall_id>. If this content is still needed, call extract_compacted_tool_result with this ID or re-run the tool.]
原文仍留在内存里,模型可用 extract_compacted_tool_result(toolcall_id) 取回——这是「先廉价回收、不丢信息」的关键。只在替换后确实更短时才替换。
第二级:摘要压缩¶
切点选择保证尾部不以孤立的工具结果开头,且必要时把最后一条用户消息强行保留在尾部(部分服务端要求请求中必须存在用户消息)。摘要么由一个子智能体产出:
Agent.inherit(agent, share_display=False, copy_toolbox=False, copy_command=False)
# 关闭 auto_compact / enable_extensions、开启 auto_confirm
# 待压缩的那批消息直接塞进它的会话,系统消息里的持久段落会被剥掉
# execute(max_iterations=1)
摘要要求以 markdown 分节输出(system_context、overview、key_facts、user_preferences、decisions、pending_tasks、open_questions、tone_context),总量控制在 1024 token 内。
成功后摘要写进 SystemPrompt.content 并把 is_compressed 置真:请求时它被包进一段「Earlier conversation history has been compacted into the summary below」模板(COMPACTED_SYSTEM_PROMPT),persist_sections(含 AGENTS.md)原样追加在后面。同时 total_tokens 置空,compaction_counter.summary_rounds 加一,工具轮次计数归零。
无内容可压缩或摘要失败时,会话保持原样,返回状态分别为 NOTHING_TO_CONDENSE / SUMMARIZE_FAILED。
参数一览¶
策略参数是 AutoCompactor 的字段(代码层),开关与阈值是配置(用户层):
| 位置 | 名称 | 默认 | 说明 |
|---|---|---|---|
AutoCompactor |
toolcall_keep_max |
12 | 保留最近多少条工具结果 |
AutoCompactor |
summary_keep_max |
16 | 摘要时保留的尾部消息数 |
AutoCompactor |
escalation_rounds |
2 | 连续多少轮廉价回收后升级为摘要;回收后估算仍超过阈值 × ESCALATION_RATIO 时当轮立即升级 |
AutoCompactor |
max_retries |
2 | 摘要后仍超标时允许的额外压缩次数;每再压一轮,toolcall_keep_max 与 summary_keep_max 各自减半 |
| 模块常量 | ESCALATION_RATIO |
0.95 | 估算值需低于阈值的 95% 才算有效进展;它是 compact 模块级常量,不能按 AutoCompactor 实例调整 |
| 配置 | auto_compact.enabled |
true |
开关 |
| 配置 | auto_compact.token_threshold |
192000 | 触发阈值 |
自定义策略只需继承 CompactorAbstract 并实现 auto_compact(agent),然后 Agent(compactor=MyCompactor())。
手动触发¶
| 方式 | 行为 |
|---|---|
/compact |
只做第二级摘要压缩(等价于 compact_conversation) |
/compact toolcall |
只做第一级回收 |
agent.conversation.compact_toolcall(keep_max=12) |
编程调用第一级(Conversation 的方法),返回 ToolCallCompactResult(reclaimed_count, reclaimed_fraction) |
xun.compact.compact_conversation(agent, keep_recent=16) |
编程调用第二级(模块函数,未在顶层导出),返回 SummaryCompactResult |