跳到主要内容

上下文管理与健壮性:让长对话不崩

30 秒导读: 一个聊天 Agent 跑久了会遇到四类翻车:上下文太长塞不进模型、tool_use/tool_result 配对被打断被各家 API 拒收(返回 400)、模型陷入"反复调同一个工具"的死循环、以及用户点了取消或上下文真的溢出。本章讲 CowAgent 怎么用四道互相独立的防线把这些都兜住,让长对话跑再久也不崩。

这一章是整个 harness 里工程含量最高的一章。前面 01-agent-loop.md 讲的是"循环怎么转",02-system-prompt.md 讲的是"每轮怎么重建系统提示"。本章讲的是循环转久了会出哪些事、怎么不出事——把一个理论上会无限跑的 tool-call 循环,变成一个真的能上线、能扛住脏数据和用户乱点的东西。


1. 这是什么(零基础也能懂)

先建立直觉

把一次 LLM 请求想成寄快递:模型只有一个固定大小的箱子(上下文窗口,如 Claude 的 20 万 token)。你每多聊一轮,就往箱子里塞更多东西——历史消息、工具调用、工具返回的一大坨结果。箱子迟早会满。

而且这个箱子对摆放顺序有洁癖:模型说"我要调工具 A"(一个 tool_use 块),下一条消息就必须是"工具 A 的结果"(一个 tool_result 块),两者的 id 还得对上。少一个、错一个、顺序反了,API 直接退货报 400。

四个真实会翻车的场景

翻车场景白话后果
上下文超长箱子塞不下了API 报 "prompt is too long",请求失败
配对被打断tool_use 后面没跟上对应的 tool_result各家 API 报 400(MiniMax 报错码 2013)
死循环模型反复调同一个工具、同样的参数烧钱、卡住、永远不给用户回复
取消 / 溢出用户点取消,或箱子当场爆了若不收尾,历史留下半截 tool_use,下一轮直接崩

CowAgent 的四道防线

本章就是围绕这四类翻车,讲对应的四组机制:

  1. 上下文裁剪——箱子快满时,按"完整轮次"扔掉旧对话,绝不在工具步骤中途裁剪。
  2. 消息合法性修复——发给模型前,扫一遍历史,把断掉的 tool_use/tool_result 配对补好或删掉。
  3. 失败与死循环保护——同一个工具连续失败、或反复用同参数调用,分级熔断,严重时直接中止。
  4. 取消与溢出恢复——取消或溢出时,先补齐 tool_result 保证历史合法,再优雅收场;溢出前先把要丢的内容存进记忆。

这四道防线互相独立、各管一段,共同保证"长对话不崩"。


2. 顶层全景(四道防线在循环的哪个位置)

先看一次 run_stream 里,四道防线分别在什么时机介入。从上到下是一次请求的时间线

用户消息进来

├─ ① 循环开始前【只裁剪一次】 _trim_messages() ← 防线1:上下文裁剪
│ └─ 裁完立刻修复配对 _validate_and_fix_messages() ← 防线2:合法性修复


┌──────────────── while turn < max_turns ────────────────┐
│ _check_cancelled() ← 防线4:每轮开头探测取消 │
│ │ │
│ ▼ │
│ _call_llm_stream() │
│ ├─ 发送前再修一次 _validate_and_fix_messages() ← 防线2
│ ├─ 流式中每 8 chunk 探测取消 ← 防线4
│ ├─ 报错?→ 溢出→激进裁剪/清空;429→退避重试 ← 防线1+4
│ │ │
│ ▼ │
│ 执行工具前 _check_consecutive_failures() ← 防线3:熔断
│ ├─ 同参 5 次 / 同参连败 3 / 同工具连败 6、8 │
│ └─ _record_tool_result() 记账 ← 防线3
│ │ │
│ ▼ │
│ finally: 无论如何都补上 tool_result ← 防线2:保配对不断
│ └─ 工具成功却被反复调 → 注入"别再调了"提示 ← 防线3
└─────────────────────────────────────────────────────────┘


(取消时)_handle_cancelled() 补 tool_result + 打断标记 ← 防线4


executor.messages 同步回 agent.messages(裁剪后要整体替换) ← 上游持久化协作

怎么读这张图: 关键在于裁剪只在循环开始前发生一次(图中①),循环内部只做"修复配对",绝不再裁剪。这是整个设计的定海神针——下一节详解为什么。

各机制归属的文件

防线核心函数所在文件
上下文裁剪_trim_messages / _truncate_historical_tool_results / _aggressive_trim_for_overflowagent/protocol/agent_stream.py
token 估算_estimate_message_tokens / _estimate_text_tokensagent/protocol/agent.py
合法性修复sanitize_claude_messages / _repair_tool_use_adjacency / compress_turn_to_text_only / drop_orphaned_tool_results_openaiagent/protocol/message_utils.py
失败/死循环_check_consecutive_failures / _record_tool_resultagent/protocol/agent_stream.py
取消AgentCancelledError / CancelTokenRegistry / _check_cancelled / _handle_cancelledagent/protocol/cancel.py + agent_stream.py
持久化协作裁剪后 messages 同步回 agentbridge/agent_bridge.py + agent/chat/service.py

3. 防线一:上下文裁剪(长对话怎么塞进箱子)

3.1 核心设计:只裁一次,绝不在工具中途裁

这是本章最需要先讲清楚的一个决定。

它要解决的小问题: 如果在循环里边跑工具边裁剪,很可能把当前正在进行的 tool_use 连同它的 tool_result 一起或分开砍掉,导致配对断裂——模型下一步看到一个孤零零的"我要调工具"却没有结果,于是懵了、又调一遍,死循环。

CowAgent 的做法:run_stream 里,追加完用户消息后立刻裁剪一次,之后整个 while 循环内部再也不裁:

真实实现见 agent/protocol/agent_stream.py:370-378,注释把意图写得很直白:

# 示意,非源码:run_stream 循环开始前的顺序
self.messages.append(user_message_block) # 先加用户这轮的话
self._trim_messages() # 只在这里裁一次
self._validate_and_fix_messages() # 裁完可能断了配对,马上修
# ... 下面进入 while 循环,循环内不再调用 _trim_messages()

对应地,_call_llm_stream 内部只修复、不裁剪,函数里专门有一段注释强调这点(agent_stream.py:789-793):裁剪放在 run_stream 循环外只做一次,在这里裁会把本轮的 tool_use/tool_result 链斩断而引发死循环。

重点看: "裁剪"和"修复"是两件事。裁剪按轮次批量丢历史(可能制造断口),只在循环外做一次;修复负责把断口补上,每次发送前都做。两者配合,才既省 token 又不破坏配对。

3.2 token 怎么估算(CJK 1.5、ASCII 0.25 加权)

裁剪要先知道"现在有多少 token"。CowAgent 不调用真正的 tokenizer(太慢、还得依赖具体模型),而是用一个字符加权的粗估

思路: 中文/CJK 字符信息密度高,一个字约占 1.5 个 token;英文/ASCII 稀疏,约 4 个字符才 1 个 token(即每字符 0.25)。按文本里两类字符的比例加权求和。

真实实现见 agent/protocol/agent.py:270-288_estimate_text_tokens

# 示意,非源码:字符加权估算的核心
non_ascii = sum(1 for c in text if ord(c) > 127) # CJK、emoji 等
ascii_count = len(text) - non_ascii
return int(non_ascii * 1.5 + ascii_count * 0.25) + 1

在此之上,_estimate_message_tokensagent.py:227-268)按块类型加结构开销:image 记 1200、tool_use 记 50 再加上其 JSON 化 input 的文本量、tool_result 记 30 再加内容。这样每条消息、每个轮次都能估出一个 token 数。

3.3 三层裁剪策略(由轻到重)

_trim_messagesagent_stream.py:1619)不是一刀切,而是先轻后重,尽量少丢信息。裁剪单位是"完整轮次"——一个轮次 = 用户消息 + AI 回复 + 工具调用 + 工具结果,由 _identify_complete_turnsagent_stream.py:1352)切分,保证永远不在轮次中间断开。

三层从轻到重依次是:

触发条件动作代码位置
Step 0总是先做历史轮次里过长的 tool_result 截到 2 万字符_truncate_historical_tool_results :1415
Step 2轮次数 > max_context_turns(默认20)丢掉前一半轮次,被丢的先 flush 进记忆_trim_messages :1641
Step 3估算 token 仍超限分档处理(见下)_trim_messages :1667

Step 3 的 token 超限处理有个巧妙的分档agent_stream.py:1712-1774),阈值 COMPRESS_THRESHOLD = 5

  • 轮次少(<5):不舍得丢任何一轮,改为把每一轮都压缩成纯文本——只留用户问了什么、AI 最后答了什么,中间那堆臃肿的工具交互全部剥掉(用 compress_turn_to_text_only)。
  • 轮次多(≥5):直接丢前一半。轮次够多时,最老的那些没那么关键,保留近一半的完整工具链更有用。

无论哪种丢弃,被丢的轮次都会先调 flush_memory 存进当日记忆,并通过 _build_context_summary_callback:1569)把一段摘要注入到保留下来的第一条用户消息里——这样"被裁掉的对话"不会凭空消失,而是变成一句"[前情摘要]"留在上下文里。记忆检索本身不在本章,见 04-memory-knowledge.md

3.4 溢出时的激进裁剪

上面是"发送前的预防性裁剪"。如果预估失准、API 真的返回了溢出错误,还有一层兜底的激进裁剪 _aggressive_trim_for_overflowagent_stream.py:1467),比常规裁剪更狠:

  1. 所有 tool_result(含当前轮)截到 1 万字符,超长的 tool_use input 也压缩;
  2. 超长用户消息截到 1 万字符;
  3. 只保留最后 5 个完整轮次

它返回 True/False 表示"还有没有东西可裁"——若已经无可再裁,就返回 False,交给上层去清空历史(见 §6)。


4. 防线二:消息合法性修复(配对断了怎么补)

4.1 问题:为什么配对会断,断了为什么各家 API 都报 400

Anthropic 的硬性要求:含 tool_use 的 assistant 消息,下一条必须是列出对应 tool_result 的 user 消息,且覆盖每一个 tool_use id。OpenAI、MiniMax 也有各自等价的约束。

配对会断,通常来自:裁剪切在了边界上(保留的最后一轮以 tool_use 结尾,它的 tool_result 在被丢的轮次里)、持久化写坏、上一轮工具执行时进程被打断等。断了的直接后果就是下一次请求被 API 退货:Claude/OpenAI 报 400,MiniMax 专门返回错误码 2013("tool result's tool id not found")。

4.2 sanitize_claude_messages:发送前的体检 + 修复

agent/protocol/message_utils.py:127sanitize_claude_messages 就地把消息列表修好,四步走:

  1. 邻接修复 _repair_tool_use_adjacency:27)——assistant 的 tool_use 后面若没跟上匹配的 user tool_result,就合成一个占位 tool_result 插进去(内容是 _SYNTH_TOOL_ERR,标 is_error)。
  2. 删开头孤儿——历史开头若是一条只含 tool_result、没有对应 tool_use 的 user 消息,删掉。
  3. 迭代删不匹配对——扫全表,tool_use id 与 tool_result id 求差集,把落单的两边都删;因为删一条可能又制造新孤儿,所以循环最多 5 次直到稳定
  4. 删完再修一次邻接——删除会破坏邻接,若确实删过就再跑一遍 Step 1。

用一段简化代码演示"补合成块"的核心想法:

# 示意,非源码:assistant 说要调 3 个工具,但只找到 2 个结果 → 给缺的那个补占位
required = ["id_a", "id_b", "id_c"] # assistant 里的 tool_use id
present = {"id_a", "id_b"} # 下一条 user 里已有的 tool_result id
missing = [i for i in required if i not in present] # ["id_c"]
for tid in missing: # 给每个缺失 id 合成一个错误结果块
inject_tool_result(tid, "session repair", is_error=True)

重点看: 修复的哲学是"宁可插一个假的错误结果,也不能让配对断着发出去"。合成块告诉模型"这一步的结果丢了,从这继续",模型能自己往下走;而一个断掉的配对会让 API 直接拒收。

4.3 两条辅助:文本压缩与 OpenAI 侧清洗

  • compress_turn_to_text_onlymessage_utils.py:295)——把一个带完整工具链的轮次压成"首条用户文本 + 末条 assistant 文本"两条纯文本消息,剥掉臃肿的中间工具交互。这正是 §3.3 里"轮次少时全压缩"用的工具。
  • drop_orphaned_tool_results_openaimessage_utils.py:243)——针对已转成 OpenAI 格式的消息,丢掉 tool_call_id 找不到对应 tool_calls[].id 的孤儿 role=tool 消息。这是给 MiniMax、OpenAI 兼容 bot 用的第二道保险。

5. 防线三:失败与死循环保护(模型抽风怎么办)

5.1 问题:模型会陷入两种循环

一是反复失败:某工具一直报错,模型不换思路,同样的调用一遍遍失败。二是成功也循环:工具明明成功返回了,模型却像没看见,用同样参数一调再调,永不收尾。两种都烧钱、都卡死用户。

5.2 分级熔断 _check_consecutive_failures

每次执行工具前,agent_stream.py:269_check_consecutive_failures 先查"历史账本"(tool_failure_history,由 _record_tool_result:332 记录,只留最近 50 条)。它返回 (should_stop, reason, is_critical),分级如下:

触发条件判定口径动作is_critical
同工具 + 同参数被调 5无论成功失败停这步,提示"结果已在之前返回"
同工具 + 同参数连败 3只数失败,遇成功清零停这步,防无限循环
同工具连败 6 次(任意参数)只数失败停这步,警告
同工具连败 8 次(任意参数)只数失败致命中止整段对话,给用户建议换方式

注意第一档"同参 5 次"专治"成功也循环"——它把成功的调用也计数,所以哪怕工具次次成功,只要参数一模一样调够 5 次照样叫停(agent_stream.py:290-292)。

is_critical=True(连败 8)时,_execute_tool 会返回 status="critical_error",主循环见到就直接 return,整段对话中止(agent_stream.py:530-533)。其余档只是把这一步标为 error 让模型换招。

5.3 "工具成功却被反复调"的收尾提示

除了硬熔断,还有一手软引导。在每轮把 tool_result 加回历史后(agent_stream.py:595-618),若发现最近 10 次里同工具+同参数成功调用已 ≥3 次,就额外注入一条 user 提示:

工具已成功执行并返回结果。请基于这些信息向用户做出回复,不要重复调用相同的工具。

这比硬停更温和——它假设模型只是"忘了自己已经拿到结果",推它一把去收尾。类似地,跑满 max_turns(默认 100)时会注入一条"已达步数上限,请直接用文字总结"的提示逼模型收口,并在事后把这条注入提示从持久化历史里删掉,避免污染记录(agent_stream.py:642-685)。


6. 防线四:取消与溢出恢复(优雅地停下来)

6.1 取消:一个 Event,多处安全探测

机制: 用户点"取消"按钮或发 /cancel,会通过 CancelTokenRegistryagent/protocol/cancel.py:35,按 request_id 建索引、session_id 兜底的线程安全单例)把一个 threading.Event 置位。Agent 循环在若干安全检查点轮询这个 Event,一旦置位就抛 AgentCancelledError

检查点有三处,粒度层层递进:

检查点位置代价
每轮开头run_stream while 顶部 _check_cancelled() :396一次 is_set()
每次工具调用之间工具循环内 _check_cancelled() :504一次 is_set()
流式生成中8 个 chunk 探测一次 :866-890用计数器 _CANCEL_PROBE_EVERY=8 摊薄,不必逐 token 查

流式中途取消时只保存已生成的纯文本,不保存可能被截断的半截 tool_use 参数(否则又会破坏配对)。

6.2 取消后如何优雅收尾 _handle_cancelled

取消抛出的异常在 run_streamexcept AgentCancelledError 接住(agent_stream.py:687),调 _handle_cancelled:148)收尾。关键是保证历史合法:如果最后一条 assistant 消息里有还没配上结果的 tool_use,就给每个补一个 tool_result(内容 "Cancelled by user before this tool finished.",标 is_error),再追加一条 _(Cancelled by user)_ 标记。

# 示意,非源码:取消收尾——先补齐孤儿 tool_use 的结果,再打断点标记
pending = [b["id"] for b in last_assistant.content if b.type == "tool_use"]
if pending:
append_user_message([tool_result(tid, "Cancelled by user...", is_error=True)
for tid in pending]) # 补齐,保证下一轮历史合法
append_assistant_text("_(Cancelled by user)_") # 留一个清晰的停止边界

重点看: 取消不是"直接扔掉",而是"补齐再停"。因为这段历史还要留给下一轮用,任何一个孤儿 tool_use 都会让下次请求报 400。

6.3 溢出恢复:先存记忆,激进裁剪,实在不行才清空

上下文真的溢出时(API 返回错误),_call_llm_stream 的异常处理走一条专门的路径(agent_stream.py:915-1059)。溢出的识别不靠状态码(各家不一样),而是关键词匹配 is_overflow:918):context length exceededprompt is too longtoo largerequest_too_large 等一串关键词命中即判溢出。

识别后的恢复是阶梯式的:

溢出错误命中

├─ ① 先 flush_memory(reason="overflow") 把即将丢失的上下文存进记忆 ← agent_stream.py:1026

├─ ② _aggressive_trim_for_overflow() 激进裁剪后重试一次(_overflow_retry=True)
│ └─ 裁成功 → 递归重发,多半这次就过了

└─ ③ 激进裁剪也没用 / 是消息格式错误
└─ self.messages.clear() + _clear_session_db() 清空内存 + 清 DB 脏数据
└─ 抛给用户:"历史过长已清空,请重新描述"

第 ③ 步为什么要连 DB 一起清(_clear_session_db :1777)?因为坏掉的 tool_use/tool_result 若留在 SQLite,下次请求或重启会把脏数据重新加载进来,同样的溢出/格式错误会永远复发。清 DB 是为了斩断这个"脏数据轮回"。

同一段异常处理还顺带用同样的关键词匹配判定消息格式错误is_message_format_error :1011),关键词刻意放宽以同时覆盖 Claude/OpenAI 的标准报错和 MiniMax 的 2013 错误码,且要求错误里带 400/invalid_request 才算数。格式错误走的是"直接清空+清 DB"分支,不做激进裁剪。

6.4 普通 LLM 报错的重试与退避

不是所有错误都要清空。可重试的错误(timeout、connection、429、5xx 等,is_retryable :1065)走退避重试,最多 3 次,且429 和其它错误退避时长不同agent_stream.py:1071-1080):

错误类型退避时长理由
429 / rate limit30s、45s、60s限流需要更长冷静期
其它可重试(超时/5xx等)2s、4s、6s短暂抖动,快速重试即可

另外还有一层"空响应重试":模型返回既无文本又无工具调用时,retry_on_empty 会静默重发一次(:1124-1138),因为这通常是模型在工具执行后误以为任务已完成。


7. 上游协作:裁剪后,新消息怎么正确同步回去

裁剪带来一个容易被忽略的坑,值得单独讲:executor 裁剪了自己的 messages 后,上游怎么知道哪些是"这轮新产生、需要持久化"的消息?

难点在于:executor 在裁剪之前就把用户这轮的消息 append 进去了,裁剪会让整个列表变短,所以不能简单地用"裁剪前的长度"去切片(那个下标已经越界或指错位置)。

agent/chat/service.py:221-270 给出的解法很典型:如果检测到 len(executor.messages) < original_length(说明裁过),就从列表尾部往前找到最后一条"真正的用户提问"(有 text、无 tool_result),以它为起点切出本轮新消息:

# 示意,非源码:裁剪后定位"新消息从哪开始"
if len(executor.messages) < original_length: # 裁过了
new_start = original_length # 兜底值
for idx in range(len(executor.messages) - 1, -1, -1):
if is_real_user_query(executor.messages[idx]): # 有 text、无 tool_result
new_start = idx # 用户这轮的问题一定裁不掉
break
new_messages = executor.messages[new_start:]
else:
new_messages = executor.messages[original_length:] # 没裁,直接按原长切
agent.messages = list(executor.messages) # 整体替换,别 append

关键洞察:用户这轮的提问永远属于最后一个轮次、裁不掉,所以它是一个稳定的锚点。bridge/agent_bridge.py:566-575 走的是另一条等价路径——直接用 executor 在 agent.py:469-474 里挂好的 _last_run_new_messages(同样是按"裁剪调整后的起点"切好的),并且因为用户消息已经提前持久化过(_pre_persist_user_message :799),这里还要把开头那条用户消息去重掉,避免存两遍。

agent.py:454-464 里还有一处呼应:如果 executor 因溢出/格式错误把自己的 messages 清空了,上游会把 agent.messages 也一并清掉,确保下一次请求从干净状态开始,而不是永远撞同一个溢出。

一句话: 裁剪不只是 executor 内部的事——它改变了消息列表的长度,上游必须用"锚定用户提问"的方式重新对齐,才能既不漏存、也不重存。这就是"trim 与持久化协作"的全部要点。


8. 巧妙之处(可借鉴的技术)

  • 裁剪与循环解耦。 "循环开始前只裁一次、循环内绝不裁"这一条,用最简单的方式根除了"裁剪斩断 tool 链→死循环"这类最难调的 bug(agent_stream.py:370-373)。
  • 合成占位而非丢弃。 配对断了优先合成一个 is_errortool_result 塞回去,让模型能自愈地往下走,而不是硬删导致上下文断层(message_utils.py:_repair_tool_use_adjacency)。
  • 关键词识别错误类型,不赌状态码。 溢出、格式错误都靠关键词匹配识别,天然跨模型通用;还专门把 MiniMax 的 2013 编进关键词表(agent_stream.py:918:1011-1019)。
  • "成功也熔断"。 死循环保护把成功调用也计数(同参 5 次即停),抓住了"工具成功但模型没看见、反复调"这个反直觉的死循环(agent_stream.py:290-292)。
  • 溢出恢复连 DB 一起清。 斩断"脏数据重载→永远复发"的轮回,是很多 harness 会漏掉的一步(agent_stream.py:_clear_session_db)。
  • 取消粒度分层 + 摊薄探测。 轮边界、工具间用零成本 is_set(),流式中每 8 chunk 才查一次,兼顾响应速度和开销(agent_stream.py:866-890)。

9. 边界与局限(诚实地说)

  • token 是估算不是精算。 _estimate_text_tokens 的 1.5 / 0.25 权重是经验值,与真实 tokenizer 有偏差;所以才需要 §3.4 的溢出兜底——预估失准时靠 API 真实报错再激进裁剪。
  • 裁剪会丢信息。 无论压缩成文本还是丢半数轮次,细节都会损失;靠 flush_memory + 摘要注入部分弥补,但被裁掉的工具中间过程无法完整找回。
  • 合成的占位结果是"假"的。 补进去的 tool_result 内容是错误提示而非真实结果,模型据此推理可能不准——这是"保证请求能发出去"与"结果真实性"之间的取舍。
  • max_context_turns 默认 20、max_turns 默认 100 是硬编码的经验阈值,未随模型上下文窗口自适应;超长窗口模型(如 Gemini 2M)上可能偏保守。
  • 关键词匹配可能误判。 溢出/格式错误全靠字符串关键词,理论上存在错误消息恰好命中关键词而被误分类的可能(agent_stream.py:918)。

10. 代码地图(导航索引)

主题文件路径符号名
循环开始前只裁一次agent/protocol/agent_stream.pyrun_stream_trim_messages + _validate_and_fix_messages 调用点)
三层裁剪主逻辑agent/protocol/agent_stream.py_trim_messages
历史 tool_result 截断agent/protocol/agent_stream.py_truncate_historical_tool_results
溢出激进裁剪agent/protocol/agent_stream.py_aggressive_trim_for_overflow
完整轮次切分agent/protocol/agent_stream.py_identify_complete_turns
摘要注入回上下文agent/protocol/agent_stream.py_build_context_summary_callback
token 估算(CJK1.5/ASCII0.25)agent/protocol/agent.py_estimate_text_tokens / _estimate_message_tokens
模型上下文窗口探测agent/protocol/agent.py_get_model_context_window
消息合法性修复入口agent/protocol/message_utils.pysanitize_claude_messages
tool_use/tool_result 邻接修复agent/protocol/message_utils.py_repair_tool_use_adjacency
轮次压缩为纯文本agent/protocol/message_utils.pycompress_turn_to_text_only
OpenAI 侧孤儿清洗agent/protocol/message_utils.pydrop_orphaned_tool_results_openai
分级熔断agent/protocol/agent_stream.py_check_consecutive_failures
失败账本记录agent/protocol/agent_stream.py_record_tool_result
取消异常 + 令牌注册表agent/protocol/cancel.pyAgentCancelledError / CancelTokenRegistry
取消安全探测agent/protocol/agent_stream.py_check_cancelled
取消优雅收尾agent/protocol/agent_stream.py_handle_cancelled
LLM 流式 + 溢出/重试处理agent/protocol/agent_stream.py_call_llm_stream
清空会话 DB 脏数据agent/protocol/agent_stream.py_clear_session_db
溢出前 flush 记忆agent/memory/manager.pyflush_memory
裁剪后同步/持久化协作agent/chat/service.pyhandle_message(trimmed 切片逻辑)
裁剪后同步/持久化协作bridge/agent_bridge.py_last_run_new_messages 消费 + _pre_persist_user_message

相邻章节: 循环怎么转见 01-agent-loop.md;每轮系统提示如何重建见 02-system-prompt.md;被裁掉的上下文如何进记忆、又如何检索回来见 04-memory-knowledge.md;工具与 MCP 集成见 05-skills-tools-mcp.md。全景见 index.md