阅读契约。 全文只跟踪同一个任务:“继续排查昨天的证书错误,并生成一份报告。”读完后,你应该能复述这次工作从哪里开始、 模型和工具怎样接力、进度与文件保存到哪里,以及 Summary、Context Compaction 与 Session Recall 怎样围绕同一本事件账本分工。
证据边界。
框架总览链接固定到公开快照 d3b50fd90d60b4ece8a78e89cf890a4f06d78235;上下文深读补充到
0c7774187da9330144df2a038ef18ee89ef2ae1c,实验固定到 c9aabe56c0eb1cb80e927d2a34ecc72658173cbe。
文中“工作台”“账本”“投影”是帮助理解的工程归纳,具体行为以链接源码与实验报告为准。
一、先看一个客服 Agent 上线后会遇到什么
假设一个企业客服 Agent 要帮用户排查线上错误。它先读取日志,再查询历史工单,必要时运行诊断代码, 最后生成一份报告。只调用一次模型并不难,困难的是把这件事连续、可靠地做完。
请求可能来自网页,也可能来自另一个 Agent;日志查询和代码执行会产生副作用与文件;确定性排查步骤不能全靠模型临场发挥; 服务重启后要继续原任务;第三天用户只说“还是昨天那个错误”,系统还要找回必要背景。tRPC-Agent-Go 的框架边界, 就是把这些“模型之外”的生产责任接进同一条运行主线。
可以先把它理解成包在 Agent 外面的一套任务运行系统。模型只负责根据当前材料生成下一步; Agent 负责把下一步变成一段可执行工作;tRPC-Agent-Go 还要接住入口、会话、工具、文件、恢复和收尾。 从外部请求被接住,到最终结果持续返回并完成持久化,这一整段生命周期叫作一次 run。 一次 run 可能经历多次模型调用和工具执行,不能把它误解成一次 completion。
二、Runner 不是一根管道,而是一张运行工作台
用户说“继续查昨天的证书错误”时,系统不能立刻把这句话丢给模型。它要先认出用户和会话,找回昨天的记录, 选择负责排障的 Agent,准备长期资料与文件存储,再保证执行过程可以取消、持久化和向外流式返回。 tRPC-Agent-Go 把这张工作台交给 Runner 管理。
Runner 管理的一次 run
1. 认出用户,打开当前工单
2. 选择 Agent,建立本轮工作单
3. 准备长期资料与文件服务
4. 接收执行进度,持久化并流式返回
5. 完成后判断是否要总结、抽取事实或复盘方法
这不是另一张 package 清单,而是一张工作分配图。Session、Memory 和 Artifact 不是远离 Runner 的“旁路服务”: Runner 自己持有这些 service,并在建立本轮工作单时把它们交给 Agent 使用。Evolution 也由 Runner 持有, 但它不进入前台工作单,而是在 run 完成后接手复盘。
2.1 Runner:先开工单,再监督整轮工作
Runner 做的第一件事不是推理,而是把一次请求变成可管理的工作。它找回或创建 Session,选择 Agent,保存本轮输入, 然后登记这次 run,后面才能处理取消、并发和完成状态。可以把它想成客服中心的值班主管:主管不亲自判断证书为什么失效, 但他要确保工单、处理人、资料、产物和回报渠道都在。
源码里的 runner 字段
直接保存 Agent 注册表以及 Session、Memory、Artifact、Evolution 等 service。
Run
负责从会话装载一路走到 Agent.Run;它创建的本轮工作单叫作
Invocation。
Invocation 不是另一位执行者,而是把“谁来做、处理哪条消息、使用哪个会话和哪些服务”装在一起的运行上下文。
2.2 Agent:拿到工作单后,决定怎样把任务做完
Agent 是真正接下工作单的执行者。排查证书时,它决定先读历史还是先查日志、什么时候调用诊断工具、 什么时候材料已经足够并交付答案。不同任务需要不同执行方式,因此 Agent 不等于一个固定的模型循环: LLMAgent 会让模型和工具反复接力,GraphAgent 按预先编译的状态图推进,Chain、Parallel、Cycle 则组合多个 Agent。
框架用同一个
Agent interface
收束这些差异:Runner 交出 Invocation,Agent 返回一条 Event channel。这样 Runner 只监督 run,
不必知道内部是一次模型循环、一张图,还是多个子 Agent 并行执行。
2.3 Model 与 Tool:一个提出下一步,一个真正动手
模型不是整位 Agent。它只看这一刻准备好的请求,然后返回文本,或者提出“请调用日志查询工具”这样的 tool call。 真正连接日志系统、执行代码和保存文件的是 Tool;LLMAgent 把工具结果放回上下文,再问模型下一步。 因而一次排障 run 里可能出现“模型判断 → 工具行动 → 模型再判断”的多次往返。
源码边界也很克制:
model.Model
只定义生成与模型信息;真正的 provider 调用发生在
llmflow。
会话保存、工具循环和长期资料都不属于 Model。
2.4 三类状态服务:本轮档案、长期资料与文件产物
当前排障怎样继续:SessionService
Session 是这次证书排障的工单夹:用户说过什么、Agent 调过什么工具、结果和状态怎样变化,都按顺序留下。
每一条过程记录叫作 Event;SessionService 负责创建、读取和追加这些记录。用户明天回来时,Runner 先从这里恢复“昨天做到哪里”,
而不是要求模型凭空记住。结构见
Session、
session.Service
和
Event。
换了工单还应知道什么:MemoryService
Session 只回答“这张工单发生过什么”。如果用户新开一张工单,系统仍应知道“生产环境使用 Go”这样的稳定事实,
就要交给 MemoryService。它保存跨 Session 的 fact 和 episode,可以在下一轮预加载,也可以按需搜索;
但它不代替当前工单的原始记录。公开接口见
memory.Service。
报告为什么不塞回聊天文本:ArtifactService
排障脚本、日志切片和最终报告可能很大,也可能反复修改。如果全部塞进 Event 文本,既难版本化,也难让前端稳定引用。
ArtifactService 因此按 Session、文件名和 revision 保存文件。Agent 通过工具产生或读取文件,Session 只需要保留这次动作和可见结果。
对应的服务接口在
artifact.Service。
还有一类完成后才运行的服务:Evolution Service。它不为当前排障提供资料,也不保存用户事实;Runner 在 run 完成后把会话快照交给它, 让它判断“先检查证书有效期”这类做法是否值得沉淀成 Skill。后文会完整拆这条后台路径。
三、跟着一次客服排障走完一轮
3.1 请求先进入 Runner,而不是直接进入模型
网页用 AG-UI 发来“继续查昨天的证书错误,并生成报告”。协议 adapter 先把外部请求翻成 Runner 能识别的用户、会话和消息; A2A 与 OpenAI-compatible 接口也能启动 run,只是它们携带身份和历史的方式不同。协议层负责翻译,Runner 才负责运行。
3.2 Runner 打开工作台,并把服务装进 Invocation
Runner 根据 app、user、session 找回昨天的 Session,选择负责排障的 Agent,再建立 Invocation。
这张工作单包含当前消息、Session、Agent、运行选项,以及 SessionService、MemoryService、ArtifactService。
在启动 Agent 之前,Runner 还会保存当前用户输入;即使后面的模型或工具失败,这次请求也不会像从未发生过。
源码交接点见
会话装载、Invocation 与启动前持久化
和
newRunInvocation。
3.3 Agent 让 Model 与工具反复接力
Runner 调用 Agent 后,具体执行方式才开始分叉。这里假设选中 LLMAgent:它先把 Session 中需要的历史、可能预加载的 Memory、 可用工具和当前消息组装成模型请求。Model 可能先要求查证书信息;工具执行并返回结果后,LLMAgent 再次调用 Model。 Model 随后要求运行诊断代码,CodeExecutor 真正执行;如果生成报告,文件写入 ArtifactService。
用户:继续排查昨天的证书错误,并生成报告
Runner:恢复工单,选择 Agent,准备会话、长期资料和文件服务
Agent:整理当前材料,询问 Model 下一步
Model:请查询证书详情
Tool:返回证书已过期的证据
Model:请运行诊断并生成报告
CodeExecutor / ArtifactService:执行诊断,保存报告文件
Model:材料足够,生成最终说明
这段例子说明了三个边界:Runner 管整轮,Agent 管执行循环,Model 只生成某一步的文本或 tool call。
实际源码由 Runner 调
Agent.Run,
LLMAgent 再进入
LLM flow。
3.4 Event 把过程交回 Runner,完成后再启动后台工作
文本片段、tool call、tool result、错误和状态变化都会包装成 Event。Runner 一边把 Event 流式交给协议层,
一边让 SessionService 保存符合条件的完整事件;因此 Event 是运行过程的传递格式,不是又一位执行者。
持久化后的事件可能触发 summary 检查;Runner 发出完成事件后,再排 auto memory 与 evolution 任务。
对应路径在
processSingleAgentEvent、
handleEventPersistence
和
完成路径。
| Event 形态 | 协议层是否可见 | Session 是否追加 | 会不会触发 Summary 检查 |
|---|---|---|---|
| 流式文本 partial | 是,用于实时显示 | 通常不追加;取消恢复是单独的合成持久化路径 | 否 |
| 完整 user message | 是 | 是,作为本轮输入证据 | 否,用户输入本身不排总结任务 |
| 完整 tool call | 是 | 是,保留调用与 ID | 否,要等结果或最终文本 |
| 完整 tool result | 是 | 是,保留结果与 tool-call 对应关系 | 是,但 checker 仍可决定 no-op |
| 完整 assistant 文本或有效 error | 是 | 是;error 会先补成可持久内容 | 是,但可用 SkipSummarization 跳过 |
| 只有 state delta | 可见性取决于协议翻译 | 即使没有完整 response 也会追加 | 没有有效正文时不触发 |
| Runner completion | 是,标记整轮结束 | 它是收尾信号,不充当对话正文 | 改为排一次 auto memory 与 evolution |
Event ID 在 Event 被构造并注入 invocation 身份时已经存在,Runner 持久化的是同一个完整 Event;partial 不会因为每个 token
都新增一条 Session 记录。真正的判断式很短:有 state delta,或 response 完整且内容有效,才进入 AppendEvent。
Summary 又在 append 成功后做第二层筛选,所以“被流式看见”“进入原始账本”“触发后台维护”是三个不同条件。
现在可以完整复述一次 run:协议层接住请求 → Runner 打开工作台 → Agent 让 Model 与工具接力 → Event 报告过程 → Runner 保存记录并收尾 → summary、memory、evolution 在各自条件满足时处理后续状态。 用户第二天回来时,Runner 再打开同一 Session,顶部图片讲的正是旧记录与长期资料怎样进入下一轮模型视图。
四、完整走完以后,再把责任压回源码
到这里再看 package,读者已经知道每个名字在客服任务的哪一刻出现。下面这张表不再按“功能多少”排序, 而是把刚才走过的问题压成源码入口。项目的完整范围可对照 README feature map。
| 运行中的问题 | 源码 owner | 读者可以怎样理解 | 第一眼源码 |
|---|---|---|---|
| 谁建立并收尾一整轮工作? | Runner / Invocation | Runner 搭工作台并监督生命周期;Invocation 是交给 Agent 的本轮工作单。 | runner、Invocation |
| 谁决定任务采用哪种执行方式? | Agent family | LLMAgent、GraphAgent 或组合 Agent 都通过同一接口执行并返回 Event。 | Agent、LLMAgent、GraphAgent |
| 谁只负责生成下一步? | Model adapters | 把统一 Request 发给 provider,再把流式 Response 交回 Agent flow。 | model.Model、provider call |
| 本轮经过在哪里保存? | SessionService + Session / Event | Session 是工单夹,Event 是按顺序写入的过程记录,Service 负责读写。 | Session、Service、Event |
| 新工单怎样拿到稳定的旧资料? | MemoryService | 保存跨 Session 的事实和经历,通过预加载或工具按需取回。 | memory.Service |
| 报告和脚本怎样独立保存? | ArtifactService | 按 Session、文件名和 revision 保存文件,不把文件本体塞进聊天记录。 | artifact.Service |
| 模型提出动作后,谁真正执行? | Tool / Skill / CodeExecutor | 把查询、工作流和代码执行变成 Agent 可以调用的受控能力。 | tool、skill.Repository、CodeExecutor |
| 本轮方法怎样变成以后可复用的 Skill? | Evolution Service | 完成后复盘 Session,把程序性经验送入受控的 Skill 发布流程。 | evolution.Service、WithEvolutionService |
| 网页、其他 Agent 和兼容客户端怎样接入? | Server adapters | 把各自的身份、历史与事件格式翻成 Runner 调用,再把 Event 翻回协议。 | AG-UI、A2A、OpenAI-compatible |
表格只是在压缩已经走过的流程。真正要记住的是:Runner 搭工作台并持有服务,Agent 接工作单,Model 只生成下一步, Tool 真正行动,Event 把过程带回 Session。后面的三篇深读,就是沿着这本事件账,依次处理“当前会话太长”、 “未来会话仍需稳定事实”和“以后任务要复用方法”三种压力。
五、这张运行工作台怎样扩展成完整框架
基础 run 讲清楚后,其他机制就能按问题自然出现:排障路线需要确定性时,Agent 可以换成 GraphAgent; 需要查询、运行代码和复用操作手册时,给 Agent 增加 Tool、CodeExecutor 与 Skill;生成文件时由 ArtifactService 保存; 同类任务反复出现时,Evolution 在 run 后学习方法;需要接网页、其他 Agent 或兼容客户端时,再由协议 adapter 包住 Runner。 下面只挑这些会改变应用写法的路径深入。
5.1 GraphAgent:流程图先落成状态机,再回到 Agent 接口
先想一个最朴素的做法:让模型在 system prompt 里“按 A、B、C 步执行,必要时并行,最后汇总”。这个做法在 demo 里可行, 但上线后会碰到几个硬问题:条件分支谁保证只走该走的路?并行分支什么时候算结束?中断之后如何恢复? 工具节点、LLM 节点、子 agent 节点的事件怎样回到同一条 session 里?
tRPC-Agent-Go 的做法,是把“下一步走到哪里”从 prompt 里拿出来,交给一张由 Go 代码定义的状态图。 图里的一个节点可以是普通函数,也可以调用模型、工具或另一个 Agent;节点之间的边负责表示顺序、分支和汇合。 图在运行前还会检查结构,避免把走不到的终点、无效的分支留到线上才暴露。对应实现集中在 节点注册 和 边、join、条件路由、compile。
| 产品问题 | runtime 动作 | 保护的不变量 | 源码入口 |
|---|---|---|---|
| 业务想要一个可复现的多步流程。 | StateGraph 把节点、边、条件分支和 join 编译成可执行图。 |
流程结构由 Go 代码验证,不靠模型临场遵守提示词。 | state_graph.go |
| 应用仍希望像普通 agent 一样调用这张图。 | New 把 StateGraph、channel buffer、并发、checkpoint saver 和 execution engine 交给 executor。 |
Graph 不变成另一套入口;它仍实现统一的 agent.Agent 运行语义。 |
graphagent.New、NewExecutor |
| 一轮请求要带着 session 历史、summary、filter、父 agent 信息进入图。 | GraphAgent.Run 创建初始 state;createInitialState 复用 content processor,把消息、summary、分支和历史过滤接进来。 |
图内节点看到的上下文,和普通 LLMAgent 的模型可见上下文保持同一套历史规则。 | GraphAgent.Run、createInitialState |
| 图执行要支持并发、checkpoint、resume、barrier 事件和错误收束。 | Executor.Execute 为每次运行创建 ExecutionContext,过滤完成 / barrier 事件,准备 checkpoint 和 pending writes,再进入 DAG 或 BSP 执行循环。 |
executor 可以被重复调用;每次运行的可变状态不会泄漏到下一次运行。 | Execute、executeGraph |
所以 GraphAgent 不是“多一种 agent 写法”。它真正解决的是流程所有权:模型负责生成内容和工具参数,
runtime 负责路由、并发、join、checkpoint、resume 和图事件。这个边界一旦清楚,后面看长上下文就更自然:
summary 和 session history 不是图外面的附属品,而是在图初始 state 里被同一套 processor 投影进去。
5.2 Skill + CodeExecutor + Artifact:把能力变成可执行工作区
再看 Skill。如果把它理解成“prompt 片段库”,就会错过框架里最有价值的一层:它把一套能力拆成说明、
可选文档、可选脚本和可执行工作区。模型不需要一开始读完所有材料,也不应该直接拿到任意 shell 权限。
先读说明,再决定要不要执行
还是回到证书排障。Agent 发现自己需要生成一份诊断报告时,第一步不是立刻执行脚本,而是先从 Skill 仓库找到
“生成诊断报告”这项能力,读取它的说明;如果说明还引用了格式规范或排障手册,再按需打开那些文档。
加载结果会记进当前 Session,因此下一次模型调用知道自己已经读过什么,不必把整座资料库反复塞进 prompt。
这条渐进加载路径由
FSRepository.Refresh
和
skill_load。
真正动手时,才打开受控工作区
“知道怎么做”和“真的执行”是两种权限。只读型 Agent 可以只获得 Skill 说明;需要运行脚本的 Agent,才会获得工作区和进程控制工具。 LLMAgent 会根据配置组装这层工具面,而不是让每个 Skill 默认拥有任意 shell。这样,同一份能力既能作为知识被阅读, 也能在明确授权后变成一次可观察、可取消、能收集输出的执行。源码入口是 tool surface 构建、 skill profiles 和 Skill 工具装配。
| 一步运行 | runtime 在做什么 | 为什么不是 prompt 拼接 |
|---|---|---|
| 模型决定需要某个 skill。 | skill_load 记录加载状态和可选文档,而不是执行脚本。 |
能力说明可以渐进展开,session 里也能看到“加载过什么”。 |
| 模型要真正运行 skill。 | skill_run 可要求先 load,再应用 artifact 保存覆盖,准备 workspace,执行程序,收集输出和 manifests。 |
执行入口有状态、参数、输出文件和 artifact 引用,不是自然语言里的一段建议。 |
| 脚本需要一个可写目录。 | stageSkill 创建可写工作副本;命令策略用 allow / deny 和 CleanEnv 控制执行环境。 |
runtime 能区分 skill 源文件、工作副本和进程环境,避免把仓库目录当临时目录乱写。 |
| 运行产生报告、图片或中间文件。 | workspace_save_artifact 把已有 workspace 文件保存成 artifact,返回 artifact:// 引用,并在 state delta 里记录。 |
大文件不需要内联进模型消息;后续 UI、会话或工具可以按引用取回。 |
到这里,三个容易混淆的概念就能分开了:CodeExecutor 是“机器怎样运行代码”,workspace 是“这次运行在哪个临时工作现场发生”, ArtifactService 则是“哪些结果要离开现场,作为报告或文件长期保存”。以证书排障为例,脚本在工作区生成报告,执行器负责跑脚本, 最后 ArtifactService 才把报告变成带 app、user、session 归属的持久产物。可沿着 Skill 执行入口、 工作副本准备 和 文件持久化 顺读这条链路。
5.3 Evolution Service:Skill 不只会被加载,也会被后台学习出来
有了 Skill 工作区以后,下一个问题自然出现:新 skill 从哪里来?最简单但危险的做法,是让前台 agent 在完成任务时顺手改
SKILL.md。这样会把用户请求、文件写入、长期事实、一次性输出和库维护混在一起:模型可能把某个用户事实写成通用技能,
也可能为了当前任务写出不可复用的流程。
tRPC-Agent-Go 把复盘安排在 run 完成之后。Runner 先把当前 Session 交给后台 worker,让用户及时拿到结果;worker 只阅读 “上次复盘以后新增的部分”,再寻找值得沉淀的证据,例如多次工具协作、用户纠正,或一次失败后成功恢复。 证据不足就跳过,证据充分才让 reviewer 判断能否形成通用方法。这样,Evolution 学的是“这类任务以后怎样做”, 而不是复制“这个用户刚才说了什么”。源码可以从 Runner 的完结后入队、 增量复盘 和 复盘门槛 开始读。
| 学习阶段 | runtime 做什么 | 保护的不变量 | 源码入口 |
|---|---|---|---|
| 完成后排队 | Runner 在 completion 后排 auto memory,也排 evolution learning job;请求上下文被 detach,worker 自己管理超时。 | 前台响应不被 review 阻塞,后台学习不会因为请求结束而立刻取消。 | completion hook、worker.Enqueue |
| 构造 review 输入 | worker 只看上次 review 之后的 session delta,并给 reviewer 当前 skill library 的名称、描述和 body excerpt。 | 学习不是重读无限历史,也不是忽略已有技能导致重复造轮子。 | ReviewInput、buildUserPrompt |
| Reviewer 决策 | LLM reviewer 只能返回 skip_reason、skills、updates、deletions 的 JSON。 |
Evolution 只拥有 skill library;用户事实和 episode 留给 memory 管线。 | review prompt、ReviewDecision / SkillSpec |
| 发布或拦截 | raw decision 先经过 library-aware reconciler;启用 revision governance 后写候选 revision,但只运行应用实际配置的 Spec、Safety、Effectiveness 或 Human gate。 | 每道已配置 gate 只保护自己的不变量;未配置对应 gate 时,不能声称该风险已经验证。 | reconcileWithLibrary、processRevision、runGates |
Reviewer 的建议先与现有技能库比对;reconciler 会尽力减少重复或冲突,但它不是完整质量证明。Spec、Safety、 Effectiveness 与 Human gate 是四个独立可选组件:配置了哪一道,runtime 才执行哪一道;HumanGate 未配置就不存在人工批准步骤。 若所有 revision/gate 组件都为空,框架还保留 direct-publish 兼容路径。因此只有在相应治理已启用时, 才能把流程复述成“候选方案 → 已配置检查 → 可选批准 → 发布”;publisher 写入并刷新仓库后,下一次 run 才能加载正文。源码见 技能库对账、 发布门禁 和 外部审批。
这样看,自进化不是“模型改自己的 prompt”,而是完成任务后的技能库维护管线。 它读 session delta,写 managed skills;它可以借 evaluator outcome 做 failure-aware learning,但不会替 memory 写事实。 这也解释了为什么它应该接在 Skill 之后讲:没有 Skill 的加载、执行、隔离和刷新,自进化产物就没有自然的落点。
5.4 AG-UI:前端协议不是壳子,它重新定义输入、运行和事件翻译边界
前端接 agent 时,最容易低估协议层。一个文本流接口只要把 assistant delta 往外推就行; 但真正的 Agent 界面还要回答三类问题:这次运行开始和结束了吗?工具或图现在走到哪一步?生成的文件、等待确认的动作和取消请求怎样对应到同一次运行? AG-UI 的作用,就是把这些只存在于 runtime 里的状态翻成前端可以稳定消费的协议事件。
输入端先把浏览器送来的消息整理成一次 Runner 调用。普通用户消息可以直接成为新输入;如果浏览器是在补交外部工具结果, adapter 会把末尾连续的工具结果归到同一个 run,再交还给 Runner 继续执行。这个整理动作很重要:它保证“继续上次工具调用” 不会被误认成一段全新的对话。可先读 输入消息解析, 再看 服务装配。
运行端先登记这次执行,因而后续取消请求能找到正确的 run;然后调用底层 Runner,持续接收 Agent 发回的 Event。
如果是图中断后的恢复,adapter 也会先补齐恢复信号,再让流程往下走。整个入口在
Runner.Run
和
内部运行循环。
输出端的 translator 像一本“本次运行翻译账”。它记住哪些文本片段、工具调用和文件已经发给前端, 再把内部 Event 依次翻成开始、增量、工具进度、产物和结束等前端事件。这样重试或多段流式输出时,UI 不会把同一个调用显示两遍, 也能把报告附件挂到正确的 run 上。源码见 translator 的运行内状态 和 事件翻译主线。
这就是为什么协议层不能只看成 HTTP 外壳:UI 看到的 run id、tool call id、graph 活动状态、artifact 引用、 外部 tool result 和最终完成信号,都不是模型文本天然携带的东西。AG-UI adapter 把这些运行时事件翻译成前端能稳定消费的协议事件。
5.5 A2A 与 OpenAI-compatible:同一个 Runner,不同的 continuity contract
AG-UI 只是 server plane 的一种出口。A2A 和 OpenAI-compatible 也会进入同一个 Runner,但“怎样认出同一次连续工作”、 “历史由谁携带”、“事件怎样出站”并不相同。把三者放在一起看,才能避免把协议 adapter 理解成换一个 URL。
| 协议面 | 连续性怎样进入 Runner | 结果怎样离开 Runner | 最重要的边界 |
|---|---|---|---|
| AG-UI | RunAgentInput 带 user/tool 尾消息、runtime state、外部工具和 run 语义。 |
翻译成 graph、text、reasoning、tool、artifact、cancel 和 finish 等富事件。 | 连续性围绕前端 run 和可恢复 UI state。 |
| A2A | AgentCard 描述能力,message processor 把 ContextID 映射为 session,并把 message metadata 带入 runtime state。 |
通过 task manager 输出 submitted/status、artifact update、completed 或结构化失败。 | 连续性围绕跨 agent 的 context 与 task lifecycle。 |
| OpenAI-compatible | /v1/chat/completions 取最后一条为当前消息,把此前 messages 作为 WithMessages,用 X-Session-ID 延续 session。 |
非流式聚合内部 events;流式输出 Chat Completions SSE chunks,最后发送 [DONE]。 |
这是 Chat Completions 兼容层,不是 Responses API 的 item/event 模型。 |
A2A 入口可以接收现成 Runner,也可以从 Agent 创建 Runner;如果直接传 Runner,还必须提供 AgentCard。
buildProcessor 组装 A2A message 到 agent message、内部 event 到 A2A result 的双向 converter。
真正处理消息时,processor 从 ContextID 建 session continuity,把 metadata 复制为 runtime state,
然后把 Runner event 变成 task status、artifact 和 completion。对应源码在
a2a.New、
buildProcessor、
message 与 streaming task setup
和
task status / artifact / completion 翻译。
OpenAI-compatible 层则刻意收窄成 /v1/chat/completions:server 可以复用外部 Runner,或为 Agent 创建并拥有一个 Runner;
request converter 把最后一条消息作为当前输入,把之前的消息通过 agent.WithMessages 带入。
非流式请求也会收齐所有内部 events 再聚合;流式请求把 event 转成 SSE chunk 并以 [DONE] 收尾。
这个快照里,temperature、max_tokens 等请求级 generation 参数还不会下传,应该在创建 Agent 时配置。
源码在
server 与路由、
非流式 history / session / event aggregation
和
流式 SSE path。
所以“共用 Runner”只说明运行核心统一,不表示 wire semantics 相同。前端续跑、跨 agent task、OpenAI SDK 兼容 分别需要不同的 identity、history 和 event contract;应用迁移协议时,不能只替换 endpoint。
先有 runtime 地图,再看上下文治理才不会偏。 Graph、Skill 工作区、Evolution 和 AG-UI 都在说明同一个边界:模型不是 runtime。 Summary、Compaction 和 Recall 也不是孤立功能,而是 Runner 持久化事件、processor 组装模型请求、tool surface 暴露恢复入口、 server 层持续承接会话之后,才自然形成的一条上下文治理链路。
六、先把三篇放进同一张时间地图
到这里,证书排障已经能运行、落盘并产出文件。接下来真正棘手的问题是:昨天的对话越来越长,今天还要继续;
用户下周新开会话时,系统仍应知道他的稳定环境;以后遇到同类故障,Agent 最好还能复用这次排查方法。
这不是一个名为 memory 的开关,而是三个时间尺度不同的问题。
| 时间尺度 | 读者真正要解决的问题 | 主要机制 | 写到哪里 |
|---|---|---|---|
| 当前会话 | 旧消息太长时,怎样继续推理,又怎样找回被压掉的原文? | Summary、Context Compaction、Session Recall | Session 的原始 Events 与 Summary 边界 |
| 跨会话 | 用户下次回来时,怎样带回稳定事实与过去经历? | Memory 抽取、检索与更新 | app/user 维度的 Fact 与 Episode |
| 未来任务 | 这次成功或失败的做法,怎样变成以后可复用的技能? | Evolution review、revision 与 gate | 受管理的 Skill 版本 |
6.1 三条后台链路各自把游标写在哪里
| 链路 | 增量游标 owner | 整体失败 | 成功后影响什么 |
|---|---|---|---|
| Summary | Session.Summaries[key].Boundary | 文本与边界都不推进 | 后续 model view;同步路径可影响当前 run |
| Auto Memory | Session.State[memory:last_extract_at] | 搜索/extractor/job 失败不推进;单条写失败后整批游标仍可能推进 | App/User Memory store,供后续 Session 检索 |
| Evolution | Session.State[evolution:last_review_at] | policy/reviewer 报错不推进;完成 review 后即使 gate 拒绝也推进 | Candidate revision;publisher 写入并 refresh repository 后未来 Agent 才可见,active pointer 记录治理版本 |
三个游标都借 Session 保存“这段原始事件处理到哪里”,但它们不是同一个事务。Summary 写回 Session 的模型视图状态; Memory 写另一个用户级存储;Evolution 写 revision store。把游标放在 Session 里只是为了增量扫描,不能据此推断三类产物具有相同一致性。
因此,tRPC-Agent-Go 的深读拆成三篇:本篇先解决当前会话怎样变短且可恢复; 下一篇讲长期 Memory 怎样跨会话保存事实;最后一篇讲 Evolution 怎样把运行结果变成可验证的技能修订。 三条链路都可能读取同一段会话,但写入对象、可见时间和失败代价都不同。
七、上下文不是原始记录,而是这一轮给模型看的投影
用户第二天说“继续昨天的证书问题”时,Session 里仍保留昨天的用户消息、模型回复和工具结果。 如果每一轮都把它们原样塞给模型,请求会不断膨胀;如果直接删除旧记录,模型一旦需要某个证书序列号,系统又无处可查。 tRPC-Agent-Go 的解法是先分清原始账本和模型视图。
持久层:Session.Events
昨天的提问 -> 日志工具调用 -> 12 KB 工具结果 -> 初步结论 -> 今天的追问
摘要边界:Summary{Text, CutoffAt, LastEventID}
“已排除网络问题,正在核对证书链” + “压缩到 event-42”
本轮模型视图
Summary 文本 + event-42 之后的新消息 + 当前用户输入
Session
同时持有 Events 与按 filter key 保存的 Summaries;
content processor
组装请求时先读取 Summary 边界,再只追加边界之后的 Events。换句话说,Summary 改的是模型这一轮看到什么,
不是把 Session 的原始事件改写成一段短文本。
这里要先把三个容易混用的词拆开。History 是 Session 里已经发生过的事件; Summary 是一份带覆盖边界的持久交接说明;Context 则是 processor 为这一次模型调用临时拼出的输入。一次调用结束后,Context 可以丢弃,History 仍是原始事实,Summary 则留给后续调用复用。 因而同一个 event 可能还在 History 中,却因为落在 Summary 边界之前而不再出现在 Context 中。
| 对象 | 谁拥有 | 允许有损吗 | 下一轮怎样看见 |
|---|---|---|---|
Session.Events | Session Service | 不应因压缩而改写或删除 | processor 投影,或 Recall 按需读取 |
Session.Summaries[key] | Session Service | 允许概括,但必须带覆盖边界 | 作为 system/user 上下文注入 |
model.Request.Messages | 当前 LLM flow | 允许裁剪、替换和重建 | 只对本次模型调用有效 |
先守住一个不变量:压缩可以有损,原始证据不能因此消失。 后面的 Summary、Context Compaction 与 Session Recall,分别负责“概括旧事实”“丢掉昂贵载荷”和“按需回原账本取证”。
八、Summary 平时滚动推进,而不是反复总结全文
第一次排障进行到一定长度后,系统可以把“已经完成的部分”压成一段交接说明。下一次再触发总结时,最笨的做法是把全部历史重新交给模型; 会话越长,总结本身越贵,也更容易把旧事实反复改写。Rememorio 实现的滚动 Summary 保存了一个边界,下一次只处理边界之后的新事件。
8.1 一次滚动更新具体发生什么
| 步骤 | 证书排障里的变化 | 源码责任 |
|---|---|---|
| 1. 判断是否触发 | 事件数、token 或上下文占用没有过线时,本轮什么也不做。 | SessionSummarizer |
| 2. 计算增量 | 读取旧 Summary,只挑上次边界之后新增的证书检查记录。 | SummarizeSession |
| 3. 合并交接说明 | 把旧 Summary 当作已知背景,用新事件更新结论,而不是重读整本账。 | buildSummaryInput |
| 4. 推进边界 | 写回新 Summary,并记录它已经覆盖到哪个事件。 | boundary selection |
这套设计允许异步总结:正常情况下,Runner 持久化事件后排一个 Summary job,前台回复不必等待。 但异步也带来边界:当前请求已经逼近模型窗口时,排队稍后总结来不及救它。因此框架还需要一条请求内的临界减压路径, 这就是 Context Compaction。
8.2 边界怎样保证“只总结新增部分”
假设旧 Summary 已覆盖到 event-42,今天又写入 event-43 到 event-51。
SummarizeSession 先按旧 boundary 计算 delta,再把旧 Summary 伪装成一条 system event 放在 delta 前面。
总结模型看到的不是 51 条原始事件,而是“一份旧交接说明 + 9 条新记录”。模型生成新文本后,运行时把边界推进到本批
最后一个可覆盖事件,并将文本、UpdatedAt 和 boundary 一起写回。
更新前
summary = "已排除网络问题"
boundary = event-42
送给 summarizer
system: "已排除网络问题" // 旧 Summary
event-43 ... event-51 // 只有新增事件
更新后
summary = "已排除网络问题;证书链缺少中间证书"
boundary = event-51
把日志和锁等外围代码裁掉后,真正推进状态的源码只有下面这条主线:
// session/internal/summary/summary.go(节选)
input, ok := buildSummaryInput(ctx, m, base, filterKey, force, prev)
if !ok {
return false, nil
}
text, err := m.Summarize(input.ctx, input.session)
if err != nil {
return false, fmt.Errorf("summarize session %s failed: %w", base.ID, err)
}
if text == "" {
return false, nil
}
boundary := selectSummaryBoundary(
input.session, filterKey, prev.boundary,
input.latestBoundary, input.hasDelta,
)
writeSummary(base, filterKey, text, boundary.CutoffTime(), boundary)
return true, nil
关键不是时间戳本身,而是“文本和覆盖边界必须一起前进”。若只更新文本、不更新边界,下一次会重复总结同一批事件;
若先推进边界、却没有成功得到新文本,则会把未进入摘要的原文藏起来。源码因此只在 summarizer 返回非空文本后调用
writeSummary。完整状态转换见
SummarizeSession
与 buildSummaryInput。
这段代码把三个边界写得很清楚。buildSummaryInput 返回 false,表示 trigger 或 delta 不满足,本轮没有模型调用;
summarizer 报错或返回空文本,也不会触碰旧状态;只有拿到非空文本后,代码才计算新 boundary,并用一次
writeSummary 同时交出文本和覆盖位置。这里的“一起”是函数状态转换层面的原子性,不代表 Session 后端天然提供跨服务数据库事务。
8.3 什么情况下它明确什么也不做
滚动 Summary 不是“run 结束必调一次模型”。没有 summarizer、旧边界之后没有新事件、trigger 未达到、模型返回空文本时,
函数都返回 updated=false,原 Summary 与边界保持不动。Runner 也不会对每个 event 都排任务:它先持久化事件,
跳过用户输入、纯 tool call 和无效事件,通常在工具结果或最终 assistant 文本出现后再考虑总结。
开启请求内同步总结时,中间工具轮次还会跳过异步排队,避免同一批 delta 被两条路径重复处理。
trigger 也由 summarizer 配置,而不是框架暗中固定。应用选择 WithContextThreshold 后,默认在动态解析出的模型窗口
达到 50% 时触发,绝对阈值不低于 2,000 token;拿不到模型窗口时用 8,192 作为 fallback。应用也可以换成事件数、
自定义 checker 或不同 ratio。这里的 50% 是后台滚动 Summary 的检查线,不要与后面请求内 Compaction 的 70% 救急线混为一谈。
同一个 session/filter 的并发总结由一把细粒度锁串行化。锁不是为了让所有会话排队,而是防止两个 worker 同时读取
event-42,各自总结到 event-51,随后互相覆盖。不同 filter key 仍可拥有各自的 Summary:
一个 Graph 分支可以只概括自己的事件,全会话 Summary 则在满足级联条件时单独推进。由此,“一份 Session”不必强迫
所有分支共享一段混杂摘要。
8.4 异步、省缓存和救急是三条不同路径
| 路径 | 什么时候用 | 是否阻塞当前回复 | 下一次何时可见 |
|---|---|---|---|
| run 后异步 Summary | 正常完成一轮,trigger 达标 | 否 | 后台写入成功后的后续请求 |
| intra-run Summary | 显式开启,工具循环中也需刷新上下文 | 是,发生在下一次模型调用前 | 同一个 run 的下一轮 LLM |
| 压力触发 Compaction | 请求已接近 context window | 是 | 当前模型调用重建后的请求 |
cache-safe forking 解决的是另一种成本:总结请求尽量沿用当前 model request 的稳定前缀,只在尾部追加压缩指令, 让支持 prompt cache 的 provider 有机会复用前缀。若 parent request 不存在或放不进 summary model 的窗口,才退回有界的独立总结输入。 它不改变 Summary 的语义,只改变“怎样把输入送给总结模型”。
九、Context Compaction 先清理工具载荷,再决定是否同步总结
继续看同一个任务。日志工具刚返回 12 KB 原文,其中绝大多数已经被模型读过并形成“证书链缺少中间证书”的结论。 旧工具结果通常是请求里最胖、又最容易安全减重的部分。tRPC-Agent-Go 因而不是一过阈值就立刻把整段会话重新总结, 而是先在构造模型视图时做三轮由便宜到昂贵的处理。
| 轮次 | 什么时候改 | 怎样改 | 保护什么 |
|---|---|---|---|
| Pass 0:策略清理 | 应用明确把某类工具列为可清理。 | 把命中的结果替换成成功占位符。 | 应用自己的工具语义。 |
| Pass 1:历史结果清理 | 旧工具结果超过阈值;默认阈值 1024 token。 | 保留最近 1 个已完成请求,其余大结果替换为可恢复占位符。 | 最近推理连续性,避免重复副作用工具。 |
| Pass 2:超大结果截断 | 任何工具结果超过应用显式配置的上限。 | 保留头尾,中间换成截断标记;推荐值 8192,但默认关闭。 | 当前请求不会被单个极端结果撑爆。 |
9.1 三轮只改本次请求的副本
三轮顺序与默认值写在
context_compact.go 的常量与占位符
以及
compactIncrementEvents。
占位符不会只写一句“内容已删除”:它说明工具曾成功执行、结果已经被消费,还携带 event_id、
tool_call_id 与工具名。若当前工具面暴露了 session_load,模型就能按引用找回原载荷;
否则提示只允许重跑只读或幂等工具。这个细节是在保护副作用边界,避免模型把“被压掉”误解成“调用失败”而重复执行。
compactIncrementEvents 先复制 event slice;真正替换 message content 时,helper 还会克隆嵌套的
Response,因此 Session Service 里的原 event 不变。
Pass 0 只执行应用明确声明的工具名策略,keep allowlist 又能挡住不应清理的工具;Pass 1 保护当前请求和最近若干完整请求;
Pass 2 才允许处理当前请求中的极端大结果,而且默认阈值为 0,也就是不开启。框架把这些默认值设得保守,是因为工具结果
可能是模型尚未消费的新证据,过早替换会让下一步推理失去依据。
// internal/flow/processor/context_compact.go(节选)
compacted := make([]event.Event, len(events))
copy(compacted, events)
if forceCleanActive {
passEvents, passStats := applyForceCleanToolResultPass(
ctx, compacted, protectedRequestIDs, cfg,
)
compacted = passEvents
stats = mergeContextCompactionStats(stats, passStats)
}
if pass1Active {
passEvents, passStats := applyHistoricalToolResultPass(
ctx, compacted, protectedRequestIDs,
cfg.ToolResultMaxTokens, cfg,
)
compacted = passEvents
stats = mergeContextCompactionStats(stats, passStats)
}
if pass2Active {
passEvents, passStats := applyOversizedToolResultPass(
ctx, compacted, currentKey,
cfg.OversizedToolResultMaxTokens, cfg,
)
compacted = passEvents
stats = mergeContextCompactionStats(stats, passStats)
}
return compacted, stats
第一、二行只完成了第一层隔离:后续 helper 改的是新的 slice 元素,而不是直接覆盖传入的
events[i]。但 event.Event 里还有 Response 指针,浅拷贝本身不能证明嵌套消息安全。
真正发生替换时,rewriteToolResultEventMessages 在第一次命中后才 clone Response,再把新消息写进 clone:
clonedResponse := evt.Response
// ...先计算 msg 是否需要改写
if !choiceChanged {
clonedResponse = evt.Response.Clone()
choiceChanged = true
}
clonedResponse.Choices[j].Message = msg
// ...
evt.Response = clonedResponse
因此完整不变量来自“两层复制”:slice copy 隔离 Event 替换,Response.Clone() 隔离嵌套内容修改。
三个 if 又把策略顺序固定下来;某一轮没启用就完全跳过,不会靠 helper 内部猜测。
stats 与副本一起逐轮合并,让可观测数据描述真实发生的替换,而不是只报最终 token 数。
Session 中的 event-37(不变)
tool: certificate_scan
content: "... 12 KB 原始结果 ..."
本次 request 中的 event-37(Pass 1 后)
tool: certificate_scan
content: "调用已成功且结果已被消费;event_id=event-37;
如需原文,使用 session_load 分片读取"
9.2 工具结果清理后仍然过线,才同步推进 Summary
请求投影完成后,LLM flow 会估算消息 token。默认在达到模型上下文窗口的 70% 时触发同步检查; 条件不满足、没有 Session Service、不能安全重建请求时都直接跳过。真正触发后,它把当前完整请求作为 parent request 交给 cache-safe Summary,等待 Summary 边界推进,然后从 content processor 之前保存的快照重新构造请求。
旧请求:旧 Summary + 大量边界后事件 + 当前输入 + tools
|
| token >= 0.7 * context window
v
cache-safe Summary:沿用 parent request 的稳定前缀,只追加压缩指令
|
v
新请求:新 Summary + 新边界后的少量事件 + 当前输入 + tools
// internal/flow/llmflow/llmflow.go(省略诊断事件)
before := snapshotSummary(invocation.Session, filterKey)
err := invocation.SessionService.CreateSessionSummary(
summaryCtx, invocation.Session, filterKey, false,
)
after := snapshotSummary(invocation.Session, filterKey)
updated := before.advanced(after)
if !updated {
return req
}
rebuilt := f.rebuildRequestForContextCompaction(
ctx, invocation, rebuildPlan,
)
if rebuilt == nil {
return req
}
if err != nil {
log.WarnfContext(ctx, "summary advanced in memory; persistence failed: %v", err)
}
return rebuilt
触发判断、同步 Summary 与重建分别见
maybeCompactContextBeforeLLM、
runContextCompaction
和
rebuildRequestForContextCompaction。
这里最容易混淆的边界是:Summary 是可持久化的滚动状态;Context Compaction 是在请求压力下组织清理、触发 Summary 并重建模型视图的运行策略。
这里没有用“Summary 调用返回 nil error”作为重建条件,而是比较前后快照的 advanced。原因是 no-op 也可能没有 error,
但边界没前进就没有更短的新投影;反过来,内存状态已经前进而后端持久化报错时,当前请求仍可以重建并减压,只是必须留下 warning。
rebuilt == nil 又守住最后一层:如果 processor 不能安全重放,宁可继续旧请求,也不拼一份半新半旧的 Context。
9.3 为什么同步总结后不能直接修改原请求
可重建的请求流水线
原始 request snapshot
-> content processor:注入 Summary,投影边界后 Events
-> context compaction:替换或截断工具载荷
-> 声明支持 replay 的 tail processors
-> Model
同步 Summary 只改变 Session 状态;重建会从 snapshot 重跑后三段。
Summary 成功写入,只说明 Session 的压缩状态前进了;原 model.Request 里仍然装着旧 Summary 和旧 events。
LLM flow 必须回到 content processor 之前保存的 request snapshot,重新执行“读 Summary 边界 → 投影边界后 events → 清理工具结果”,
才能得到一致的新视图。为了避免重复执行不透明的自定义 processor,框架只在 timeline 是全量视图、content processor
开启 Summary,且尾部 processor 明确支持重放时启用这条同步重建路径。
失败也按“当前请求仍要尽量完成”处理:token 没过线、Summary 没推进、服务缺失或重建条件不安全时,继续使用原请求; Summary 调用报错也不会把整轮 run 判失败。若内存中的 Summary 已推进而持久化失败,当前轮仍可使用重建后的短请求, 同时记录警告,让后续持久化与可观测链路处理。这是一条减压兜底,不是新的业务事务。
| 结果 | 当前调用 | 持久 Session | 下一次请求 |
|---|---|---|---|
| Summary 未推进或重建不安全 | 继续原请求 | 不变 | 仍从旧边界开始 |
| Summary 与持久化都成功 | 使用重建后的短请求 | 新文本 + 新边界 | 稳定读到新边界 |
| 内存已推进、持久化失败 | 可使用内存状态重建 | 仍可能是旧边界 | 复用同一 live Session 时可能看到新状态;从后端重载则看到旧状态 |
最后一行没有承诺自动修复:实现记录 warning,但不把本轮压缩变成分布式事务,也不保证下一请求一定重试成功。 应用要通过 SessionService 失败指标、Summary report 与后续持久化任务发现并收敛这种分叉。
十、Session Recall 让压缩后的模型回原始账本取证
Summary 说“证书链有问题”足以维持大多数对话,但用户若追问“昨天日志里的序列号到底是什么”,摘要可能没有这个细节。 直接把全部旧历史重新塞回上下文,会抹掉压缩收益;让模型凭摘要猜,则会破坏事实性。Session Recall 提供两步取证:先搜索, 再围绕命中的事件加载一小段原文窗口。
session_search(query="certificate serial", scope="current_hidden")
-> 命中 Summary 边界之前的 event-37
session_load(event_id="event-37", before=1, after=1)
-> 只取回该事件附近的用户消息、工具调用与工具结果
10.1 Search 先找锚点,不急着搬回整段原文
current_hidden 不是搜索“所有没显示的内容”,而是根据当前 Summary boundary,把检索时间限制在 cutoff 之前,
再过滤为当前会话里真正被 Summary 遮住的事件。实现见
searchCurrentHidden。
命中后,
session_load
按 event ID 读取前后窗口,也能在需要时用 tool-call ID 解析锚点。
搜索结果承担的是“索引卡”角色:告诉模型哪个 session、哪个 event 可能相关,并给出短片段,而不是把整个 transcript 再注入一次。复合问题会先尝试原查询,再按子句与关键词生成有界 fallback,最后按 session/event 去重合并。 这能提高专有名词和短句的召回,但结果仍只是候选证据;尤其工具输出很长时,下一步必须 load,不能拿搜索摘要代替原文。
10.2 Load 围绕锚点取窗口,也能继续分片
session_load 默认以 event_id 为锚点,读取前后有限数量的 user、assistant 与 tool 消息。
如果压缩占位符只有 tool_call_id,工具会先在当前内存 Session 查找对应 tool result event,再回持久 Session 查;
event ID 已失效但同时带有 tool-call ID 时,也会用后者重试。对于一个 event 中仍然很大的内容数组,
content_offset 与 content_limit 允许继续分页,而不是一次把 12 KB 全塞回来。
返回值还明确写着:加载到的历史是回答问题的上下文,不是新的活跃指令。这条提示在安全上很重要,因为旧用户消息或旧工具输出 可能包含已经过期、甚至恶意的命令。Recall 恢复的是证据,不应悄悄把历史内容提升成当前系统指令。
搜索无命中时返回空结果,模型应继续用 Summary 和当前证据作答或说明不足;Service 搜索报错、load 锚点不存在时,工具返回明确错误, 由 Agent 决定改查询、换 tool-call ID 重试或停止。Recall 不会因为检索失败而改写 Session,也不会把“没找到”伪装成事实不存在。
10.3 Preload 与按需 Recall 解决不同问题
框架还有另一条 preload 路径:在模型第一次作答前,用当前用户问题搜索其他会话的原始事件,把少量结果预先注入请求。
它适合“上周那个证书问题”这种跨会话追问;按需的 current_hidden 则适合同一长会话里找回被摘要遮住的细节。
两条路径的范围不同,不能只用一个“recall”名词带过。preload 逻辑见
getPreloadSessionRecallMessage。
| 问题 | 先走哪条路 | 为什么 |
|---|---|---|
| “昨天这条长会话里,序列号是多少?” | current_hidden → session_load | 答案已被当前 Summary 边界遮住,需要原地取证。 |
| “上周另一张工单最后怎样解决?” | 跨会话 preload search | 当前 Session 没有那段 event,需要在首轮回答前带回少量候选。 |
| “我通常使用哪个 Go 版本?” | 长期 Memory | 这是跨会话稳定事实,不该每次从原 transcript 重新推断。 |
压缩闭环到这里才完整。 Summary 负责形成便宜的主视图,Compaction 在窗口压力下删减请求副本,Recall 用稳定引用回原始账本取证。只有压缩没有 Recall,会丢精确事实;只有 Recall 没有压缩,模型仍要背着整本历史前进。
十一、Benchmark 要同时回答省多少、丢多少、能否找回
只报告“prompt 变短了”无法证明上下文治理有效。Rememorio 的 benchmark 把问题拆成三层: MT-Bench-101 检查滚动 Summary 的总体成本与连续性;QMSum 检查长会议被压缩后还能不能回答指定问题; LongMemEval 检查跨大量会话的精确事实能否通过按需 Recall 找回。以下结果来自已合入的 Summary benchmark report。
11.1 MT-Bench-101:短会话可能不值得总结
MT-Bench-101 包含 917 个案例、9 类多轮任务。实验使用 DeepSeek-V3.2,每两轮生成一次 Summary,比较长上下文基线与滚动 Summary。 总 token 减少 12.89%,其中 prompt token 减少 24.47%;一致性得分为 0.853,第一轮通过率为 92.3%。 但 329 个案例出现负节省,约占 35.9%。 分组后原因很清楚:四轮以上会话通常能省 28% 到 40%,两轮以内则可能因为“总结调用本身”比原历史还贵。
实验给出的不是“永远开启 Summary”,而是一条触发规则。 对短会话,最好的 Summary 是不运行;只有历史足够长,滚动边界才开始回本。
11.2 QMSum:按需找回能换回大部分答案质量
QMSum 原始测试集载入 244 个会议问答,实验保留答案证据距离会议尾部至少 80 条消息的 189 个案例, 避免最近窗口直接看到答案。模型为 GPT-4o-mini;Summary 在 40 条消息触发,主视图保留最近 20 条。
| 模式 | ROUGE-L | F1 | 平均 prompt token | 平均延迟 |
|---|---|---|---|---|
| 完整长上下文 | 0.1930 | 0.3132 | 18,986 | 4,556 ms |
| 仅 Summary | 0.1516 | 0.2238 | 888 | 2,994 ms |
| Summary + 按需 Recall | 0.1770 | 0.2774 | 3,857 | 8,656 ms |
按需 Recall 在仍节省 76.69% prompt token 的同时,挽回了仅 Summary 相对长上下文损失的 61.5% ROUGE-L 与 59.9% F1。 代价也很明确:搜索和再次加载让延迟高于另外两组。Recall 是质量兜底,不是免费加速器。
11.3 LongMemEval:原文召回决定精确事实题能不能答
LongMemEval 的 single-session-user 子集有 70 个案例;每个案例平均约 50 个会话、500 轮对话和 10.3 万 token。 这组数据刻意模拟“用户很久以后追问过去某个细节”,因此比普通多轮聊天更能暴露摘要丢失精确信息的问题。
| 模式 | ROUGE-L | LLM Judge | Exact Match | 平均 prompt token |
|---|---|---|---|---|
| 完整长上下文 | 0.1192 | 0.7386 | 0.6571 | 103,565 |
| 仅 Summary | 0.0477 | 0.0907 | 0.0143 | 445 |
| Summary + 按需 Recall | 0.2694 | 0.9000 | 0.7571 | 6,182 |
这组实验里,Summary + Recall 相对完整长上下文节省 94.04% prompt token,同时三项质量指标都更高。 原因不是“摘要比原文准确”,而是检索先从 10 万 token 里找证据,再让模型围绕少量相关原文作答。 实验还试过九段式结构化 Summary 并逐字保留用户消息,ROUGE-L 反而从紧凑 Summary 的 0.2694 降到 0.2528; 更长、更规整的摘要并不会自动胜过“短 Summary + 原文取证”。
LoCoMo 上的补充实验也显示 Session Recall 与长期 Memory 不是替代关系:Session Recall 的总体 F1 为 0.549, 高于完整长上下文和优化 Memory 的 0.469;但时间推理类别中,优化 Memory 为 0.247,Session Recall 只有 0.174。 原始事件擅长还原措辞和局部证据,结构化 Memory 更擅长把跨会话时间关系整理成可检索事实。后者留到下一篇展开。
十二、放到 Claude Code 与 Codex 旁边看,差别在恢复路径
Claude Code、Codex 与 tRPC-Agent-Go 都不会把“上下文”简单等同于完整会话记录。它们都保留更耐久的运行历史, 再为模型构造一个可以裁剪或替换的视图;真正不同的是压力出现时先牺牲什么,以及压缩后怎样恢复。
| 系统 | 压力到来时 | 压缩后的主视图 | 细节恢复 |
|---|---|---|---|
| tRPC-Agent-Go | 先替换旧工具结果,再按阈值同步推进 Session Summary。 | Summary + 边界后的 Events。 | session_search / session_load 回原始事件。 |
| Claude Code | 沿工具预算、局部清理与 auto compact 逐级减压。 | 以 compact boundary 为界的恢复记录与后续消息。 | 依赖持久 transcript、恢复记录与工具再读取。 |
| Codex | 先做输出截断与 prompt 投影,必要时执行 compaction。 | 用 replacement history 安装压缩后的 rollout。 | 持久 history 与 rollout 恢复链路负责续跑。 |
这里只比较所有权,不重复另外两篇的源码细节。Claude Code 的边界、microcompact 与 auto compact 见 Claude Code 上下文管理篇; Codex 的 prompt projection、replacement history 与 rollout 恢复见 Codex 上下文管理篇。 tRPC-Agent-Go 的独特组合是:把 Summary 作为 Session 的持久状态,再给被压缩的工具载荷和旧事件留下显式 Recall 工具面。
十三、把这套设计带回自己的 Agent
如果要把同样的思路落到别的框架,不必照搬类型名,可以按压力顺序做六个决定:先保存可恢复的原始事件; 每轮只投影模型真正需要的视图;优先清理已经消费过的大工具结果;接近窗口上限时再滚动 Summary; 为精确信息保留搜索与窗口加载入口;最后才把跨会话事实和可复用方法分别交给 Memory 与 Evolution。
上下文治理的目标不是把历史压到最短,而是在预算内保留正确的恢复路径。 下一篇沿同一个证书排障任务进入长期 Memory:哪些内容值得跨会话保存, 写入前为什么要先检索旧记忆,以及 LoCoMo benchmark 如何验证检索、更新与时间推理。
参考源码与文档
- tRPC-Agent-Go 上下文机制快照
- 请求侧工具结果压缩
- 滚动 Summary 与边界推进
- Session Recall 搜索与 原始事件窗口加载
- Summary / Session Recall benchmark 报告
- Claude Code 上下文管理源码阅读
- Codex 上下文管理源码阅读
- tRPC-Agent-Go repository snapshot
- project README feature map
- agent interface
- run invocation
- model interface
- event envelope
- graph agent
- state graph builder
- graph executor
- tool interface
- LLMAgent runtime and model handoff
- skill repository
- skill run tool
- workspace exec tool
- workspace artifact save tool
- code executor
- artifact service
- evolution service
- evolution worker
- evolution reviewer
- evolution revision store
- evolution gates
- AG-UI server
- AG-UI runner
- AG-UI translator
- A2A server
- OpenAI-compatible server
- runner runtime
- session model
- session summarizer
- internal summary orchestration
- content request processor
- LLM flow compaction
- memory API
- auto memory worker
- session search recall tool
- session load recall tool