阅读契约。 全文只跟踪同一个任务:“继续排查昨天的证书错误,并生成一份报告。”读完后,你应该能复述这次工作从哪里开始、 模型和工具怎样接力、进度与文件保存到哪里,以及 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 先从这里恢复“昨天做到哪里”, 而不是要求模型凭空记住。结构见 Sessionsession.ServiceEvent

换了工单还应知道什么: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 任务。 对应路径在 processSingleAgentEventhandleEventPersistence完成路径

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 的本轮工作单。 runnerInvocation
谁决定任务采用哪种执行方式? Agent family LLMAgent、GraphAgent 或组合 Agent 都通过同一接口执行并返回 Event。 AgentLLMAgentGraphAgent
谁只负责生成下一步? Model adapters 把统一 Request 发给 provider,再把流式 Response 交回 Agent flow。 model.Modelprovider call
本轮经过在哪里保存? SessionService + Session / Event Session 是工单夹,Event 是按顺序写入的过程记录,Service 负责读写。 SessionServiceEvent
新工单怎样拿到稳定的旧资料? MemoryService 保存跨 Session 的事实和经历,通过预加载或工具按需取回。 memory.Service
报告和脚本怎样独立保存? ArtifactService 按 Session、文件名和 revision 保存文件,不把文件本体塞进聊天记录。 artifact.Service
模型提出动作后,谁真正执行? Tool / Skill / CodeExecutor 把查询、工作流和代码执行变成 Agent 可以调用的受控能力。 toolskill.RepositoryCodeExecutor
本轮方法怎样变成以后可复用的 Skill? Evolution Service 完成后复盘 Session,把程序性经验送入受控的 Skill 发布流程。 evolution.ServiceWithEvolutionService
网页、其他 Agent 和兼容客户端怎样接入? Server adapters 把各自的身份、历史与事件格式翻成 Runner 调用,再把 Event 翻回协议。 AG-UIA2AOpenAI-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 一样调用这张图。 NewStateGraph、channel buffer、并发、checkpoint saver 和 execution engine 交给 executor。 Graph 不变成另一套入口;它仍实现统一的 agent.Agent 运行语义。 graphagent.NewNewExecutor
一轮请求要带着 session 历史、summary、filter、父 agent 信息进入图。 GraphAgent.Run 创建初始 state;createInitialState 复用 content processor,把消息、summary、分支和历史过滤接进来。 图内节点看到的上下文,和普通 LLMAgent 的模型可见上下文保持同一套历史规则。 GraphAgent.RuncreateInitialState
图执行要支持并发、checkpoint、resume、barrier 事件和错误收束。 Executor.Execute 为每次运行创建 ExecutionContext,过滤完成 / barrier 事件,准备 checkpoint 和 pending writes,再进入 DAG 或 BSP 执行循环。 executor 可以被重复调用;每次运行的可变状态不会泄漏到下一次运行。 ExecuteexecuteGraph

所以 GraphAgent 不是“多一种 agent 写法”。它真正解决的是流程所有权:模型负责生成内容和工具参数, runtime 负责路由、并发、join、checkpoint、resume 和图事件。这个边界一旦清楚,后面看长上下文就更自然: summary 和 session history 不是图外面的附属品,而是在图初始 state 里被同一套 processor 投影进去。

5.2 Skill + CodeExecutor + Artifact:把能力变成可执行工作区

再看 Skill。如果把它理解成“prompt 片段库”,就会错过框架里最有价值的一层:它把一套能力拆成说明、 可选文档、可选脚本和可执行工作区。模型不需要一开始读完所有材料,也不应该直接拿到任意 shell 权限。

先读说明,再决定要不要执行

还是回到证书排障。Agent 发现自己需要生成一份诊断报告时,第一步不是立刻执行脚本,而是先从 Skill 仓库找到 “生成诊断报告”这项能力,读取它的说明;如果说明还引用了格式规范或排障手册,再按需打开那些文档。 加载结果会记进当前 Session,因此下一次模型调用知道自己已经读过什么,不必把整座资料库反复塞进 prompt。 这条渐进加载路径由 FSRepository.Refreshskill_load

真正动手时,才打开受控工作区

“知道怎么做”和“真的执行”是两种权限。只读型 Agent 可以只获得 Skill 说明;需要运行脚本的 Agent,才会获得工作区和进程控制工具。 LLMAgent 会根据配置组装这层工具面,而不是让每个 Skill 默认拥有任意 shell。这样,同一份能力既能作为知识被阅读, 也能在明确授权后变成一次可观察、可取消、能收集输出的执行。源码入口是 tool surface 构建skill profilesSkill 工具装配

一步运行 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 hookworker.Enqueue
构造 review 输入 worker 只看上次 review 之后的 session delta,并给 reviewer 当前 skill library 的名称、描述和 body excerpt。 学习不是重读无限历史,也不是忽略已有技能导致重复造轮子。 ReviewInputbuildUserPrompt
Reviewer 决策 LLM reviewer 只能返回 skip_reasonskillsupdatesdeletions 的 JSON。 Evolution 只拥有 skill library;用户事实和 episode 留给 memory 管线。 review promptReviewDecision / SkillSpec
发布或拦截 raw decision 先经过 library-aware reconciler;启用 revision governance 后写候选 revision,但只运行应用实际配置的 Spec、Safety、Effectiveness 或 Human gate。 每道已配置 gate 只保护自己的不变量;未配置对应 gate 时,不能声称该风险已经验证。 reconcileWithLibraryprocessRevisionrunGates

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.NewbuildProcessormessage 与 streaming task setuptask 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整体失败成功后影响什么
SummarySession.Summaries[key].Boundary文本与边界都不推进后续 model view;同步路径可影响当前 run
Auto MemorySession.State[memory:last_extract_at]搜索/extractor/job 失败不推进;单条写失败后整批游标仍可能推进App/User Memory store,供后续 Session 检索
EvolutionSession.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 保存的 Summariescontent processor 组装请求时先读取 Summary 边界,再只追加边界之后的 Events。换句话说,Summary 改的是模型这一轮看到什么, 不是把 Session 的原始事件改写成一段短文本。

这里要先把三个容易混用的词拆开。History 是 Session 里已经发生过的事件; Summary 是一份带覆盖边界的持久交接说明;Context 则是 processor 为这一次模型调用临时拼出的输入。一次调用结束后,Context 可以丢弃,History 仍是原始事实,Summary 则留给后续调用复用。 因而同一个 event 可能还在 History 中,却因为落在 Summary 边界之前而不再出现在 Context 中。

对象谁拥有允许有损吗下一轮怎样看见
Session.EventsSession 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-43event-51SummarizeSession 先按旧 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。完整状态转换见 SummarizeSessionbuildSummaryInput

这段代码把三个边界写得很清楚。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_idtool_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 与重建分别见 maybeCompactContextBeforeLLMrunContextCompactionrebuildRequestForContextCompaction。 这里最容易混淆的边界是: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_offsetcontent_limit 允许继续分页,而不是一次把 12 KB 全塞回来。

返回值还明确写着:加载到的历史是回答问题的上下文,不是新的活跃指令。这条提示在安全上很重要,因为旧用户消息或旧工具输出 可能包含已经过期、甚至恶意的命令。Recall 恢复的是证据,不应悄悄把历史内容提升成当前系统指令。

搜索无命中时返回空结果,模型应继续用 Summary 和当前证据作答或说明不足;Service 搜索报错、load 锚点不存在时,工具返回明确错误, 由 Agent 决定改查询、换 tool-call ID 重试或停止。Recall 不会因为检索失败而改写 Session,也不会把“没找到”伪装成事实不存在。

10.3 Preload 与按需 Recall 解决不同问题

框架还有另一条 preload 路径:在模型第一次作答前,用当前用户问题搜索其他会话的原始事件,把少量结果预先注入请求。 它适合“上周那个证书问题”这种跨会话追问;按需的 current_hidden 则适合同一长会话里找回被摘要遮住的细节。 两条路径的范围不同,不能只用一个“recall”名词带过。preload 逻辑见 getPreloadSessionRecallMessage

问题先走哪条路为什么
“昨天这条长会话里,序列号是多少?”current_hiddensession_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.19300.313218,9864,556 ms
仅 Summary0.15160.22388882,994 ms
Summary + 按需 Recall0.17700.27743,8578,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.11920.73860.6571103,565
仅 Summary0.04770.09070.0143445
Summary + 按需 Recall0.26940.90000.75716,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 如何验证检索、更新与时间推理。

参考源码与文档