news 2026/9/13 23:46:18

Qwen Code Session Recap 设计解析:AI 编程助手的“离开后回来“会话摘要机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qwen Code Session Recap 设计解析:AI 编程助手的“离开后回来“会话摘要机制

Qwen Code Session Recap 设计解析:AI 编程助手的"离开后回来"会话摘要机制

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

Session Recap(会话回顾)是 Qwen Code 中一项"最佳努力"(best-effort)的辅助能力:当用户离开终端一段时间后回来,或在任何时刻通过/recap命令主动请求时,CLI 会基于当前会话历史生成一句 1–2 句话的"上次进行到哪了"摘要(高层面任务 → 下一步动作),并以视觉上区别于真实助手回复的样式插入会话流。本文以 docs/design/session-recap/session-recap-design.md 设计文档为骨架,结合 packages/core/src/services/sessionRecap.ts、useAwaySummary.ts、recapCommand.ts 等源码实现,完整讲解该功能的三种触发路径、提示词与结构化输出设计、历史过滤策略、并发边界、配置项与可观测性,并给出可直接落地的配置与调用方法。

读完本文,你将掌握:如何开启并调优自动 recap、何时使用/recap手动触发、远程客户端如何通过 Daemon HTTP 接口获取会话 recap,以及这套机制在"绝不打断主流程、绝不向用户抛错"约束下是如何在源码层面被保证的。

设计动机与核心原则

用户几天后通过/resume恢复一个旧会话时,仅靠重新加载历史消息并不能解决一个真实的 UX 痛点:用户需要翻过多页历史才能想起来"我当时在做什么、接下来该做什么"。Session Recap 的目标是在用户回到会话时主动呈现一句简短的摘要,其设计遵循三条原则:

  • 内容结构固定:高层任务(正在做什么)→ 下一步(接下来做什么);
  • 视觉可区分:摘要必须与真实助手回复在视觉上明显不同,避免被误认为新的模型输出;
  • 最佳努力:任何失败都必须静默处理,绝不能破坏主流程。

从实现上看,这一"绝不打扰用户"的承诺贯穿始终:核心服务函数在try/catch中兜底返回null(见 sessionRecap.ts),UI 层则保证失败时不渲染任何内容。

三种触发路径与统一入口

Session Recap 有三条触发路径,全部汇入同一个底层函数generateSessionRecap()(位于 packages/core/src/services/sessionRecap.ts),从而保证行为完全一致:

触发方式条件实现
手动命令用户运行/recaprecapCommand.ts 调用同一底层服务
自动触发终端失焦(DECSET 1004 focus 协议)≥ 5 分钟 + 焦点回归 + 流状态为IdleuseAwaySummary.ts:5 分钟失焦计时器 +useFocus事件监听
Daemon HTTP远程客户端调用POST /session/:id/recapserver.ts路由 →bridge.generateSessionRecap(ext-method 往返)→ acpAgent.ts 调用generateSessionRecap(session.getConfig(), signal)

自动触发受general.showSessionRecap设置约束(默认关闭——显式选择加入,避免在用户账单上静默增加环境 LLM 调用);而手动命令与 Daemon HTTP 路由忽略该设置,因为调用者是显式发起请求。

Daemon 访问路径

Daemon 路由采用非严格门控(non-strict-gated),与/session/:id/prompt的姿态一致——recap 消耗 token 但不改变任何状态。能力标签session_recap会在/capabilities.features上通告该路由(见 capabilities.ts)。SDK 侧提供了两个辅助方法:

  • DaemonClient.recapSession(sessionId, opts):见 DaemonClient.ts,直接POST /session/:id/recap
  • DaemonSessionClient.recap(opts):见 DaemonSessionClient.ts。

完整的线上协议与错误封装见 docs/developers/qwen-serve-protocol.md 中POST /session/:id/recap一节。

需要特别注意的是 SDK 文档中明确记录的契约细节(DaemonClient.ts):

  • 非严格变更门控:姿势与/session/:id/prompt相同(花费 token 但不改动状态);
  • 绕过默认 30s 超时recapSession直接调用_fetch,不套用每调用fetchTimeoutMs包装器,因为底层 side-query 在慢模型下可能超过默认 30 秒;
  • 向前兼容:旧版本 daemon(不支持 recap)会返回 404——调用前应先预检caps.features.session_recap
  • recap可能为null:历史过短或模型瞬时失败时,返回 200 但recap: null(见 types.ts 中DaemonSessionRecapResult的契约)。

v1 的取消语义

v1 中不提供取消(cancellation absent):路由不监听 HTTP 客户端断开,没有把AbortSignal穿入bridge.generateSessionRecap,ACP 子进程处理器向核心助手传入的是一个永不被中止的new AbortController().signal(见 acpAgent.ts),目前尚无跨进程中止管线。唯一的"天花板"是 bridge 侧 60 秒的SESSION_RECAP_TIMEOUT_MS兜底超时,以及针对 ACP 通道死亡的传输关闭竞态。仅单独接线 HTTP 侧的 AbortController 只是表面功夫——子进程侧的 LLM 调用仍会跑完,因此在缺少跨进程中止组件的情况下无法实现端到端取消。这对 v1 是可接受的:recap 是短查询(单次尝试 side-query,maxOutputTokens: 300,典型耗时约 1–5 秒)。未来可以引入基于 request-id 的取消 ext-method,在带宽成本合理时打通完整的端到端取消。

架构与文件职责

设计文档给出了完整的调用链架构图,核心流程为:

AppContainer.tsx isFocused = useFocus() isIdle = streamingState === Idle ├─→ useAwaySummary({enabled, config, isFocused, isIdle, addItem}) │ └─→ 5 min blur timer + idle/dedupe gates │ ↓ └─→ recapCommand (slash) ─→ generateSessionRecap(config, signal) ↓ packages/core/services/sessionRecap.ts ↓ GeminiClient.generateContent (fastModel + tools:[]) addItem({type: 'away_recap', text}) ─→ HistoryItemDisplay └─ AwayRecapMessage rendered inline like any other history item (※ + bold "recap: " + italic content, all dim); scrolls naturally with the conversation

涉及的文件与职责对应关系如下:

文件职责
packages/core/src/services/sessionRecap.ts一次性 LLM 调用 + 历史过滤 + 标签提取
packages/cli/src/ui/hooks/useAwaySummary.ts自动触发 React hook
packages/cli/src/ui/commands/recapCommand.ts/recap手动入口
packages/cli/src/ui/components/messages/StatusMessages.tsxAwayRecapMessage渲染器(+ 加粗recap:+ 斜体内容,全部弱化色)
packages/cli/src/ui/types.tsHistoryItemAwayRecap类型
packages/cli/src/ui/components/HistoryItemDisplay.tsxaway_recap历史项分派给渲染器
packages/cli/src/config/settingsSchema.tsgeneral.showSessionRecap+general.sessionRecapAwayThresholdMinutes设置

值得注意的 UI 细节(源码注释明确说明):AwayRecapMessage复刻了 Claude Code 的 away-summary 渲染方式——固定 2 列宽的前缀,加粗的recap:标签 + 斜体内容,全部使用次要色(dim)。它作为普通历史项渲染,随对话自然滚动,而不是固定在输入框上方(见 StatusMessages.tsx)。HistoryItemAwayRecap的注释同样强调:作为常规历史项内联渲染,与 Claude Code 的away_summary消息一致,随会话滚动、不粘顶(types.ts)。

Prompt 设计:让模型只当一个 recap 生成器

System Prompt

generationConfig.systemInstruction替换主 Agent 的系统提示词,使模型在这一次调用中只充当 recap 生成器,而不是编码助手。

需要注意:GeminiClient.generateContent()内部会经过getCustomSystemPrompt(),将用户的记忆(QWEN.md / 托管 auto-memory)作为后缀追加。因此最终 system prompt 是recap prompt + 用户记忆——这对 recap 是有用的项目上下文,而非泄漏。

源码中RECAP_SYSTEM_PROMPT(sessionRecap.ts)与设计文档的要点一一对应:

  • 40 词以内、1–2 句平实句子(无 markdown / 列表 / 标题);中文场景约按 80 字符预算;
  • 第一句讲高层任务,然后给出具体的下一步动作;
  • 明确禁止:罗列已完成事项、复述工具调用、状态汇报;
  • 匹配对话的主导语言(英文或中文);
  • 输出包裹在<recap>...</recap>标签内,标签外不得有任何内容。

源码中的示例:

<recap>Debugging the auth retry race condition. Next: add deterministic timing to the integration test.</recap>

用户侧 prompt 同样强制约束:'Generate the recap now. Wrap it in <recap>...</recap>. Nothing outside the tags.'(sessionRecap.ts)。

结构化输出与提取

模型被要求把答案包裹在<recap>...</recap>中。原因:部分模型(GLM 家族、推理模型)会在最终答案前写一段"思考"文本,直接返回原始文本会把那段推理泄漏到 UI 中。

extractRecap()提供三级回退(sessionRecap.ts):

  1. 双标签齐全:取<recap>...</recap>之间的内容(首选路径,正则/<recap>([\s\S]*?)<\/recap>/i);
  2. 仅有开标签(例如maxOutputTokens截断了闭标签):取开标签之后的所有内容;
  3. 完全没有标签:返回空字符串 → 服务返回null→ UI 不渲染任何内容。

第三级是"跳过而非展示错误内容"策略——展示模型的推理前导文本比不展示 recap 更糟糕。

调用参数

参数原因
modelgetFastModel() ?? getModel()recap 不需要前沿模型
tools[]一次性查询,不涉及工具调用
maxOutputTokens300为 1–2 句短句 + 标签留足余量
temperature0.3基本确定性输出,保留少量自然变化
systemInstruction上述仅 recap 的提示词替换主 Agent 的角色定义

从源码看,这些参数经由runSideQuery生效(sessionRecap.ts),其中purpose: 'session-recap'maxAttempts: 1——注释明确说明:recap 是"best-effort cosmetic"(尽力而为的外观功能),不值得消耗默认的 7 次重试。

历史过滤:只把"对话"喂给模型

geminiClient.getChat().getHistory()返回的Content[]包含多种内容:

  • user/model文本消息
  • modelfunctionCall部分
  • userfunctionResponse部分(可能包含完整文件内容)
  • model的 thought 部分(part.thought/part.thoughtSignature,即模型的隐藏推理)

filterToDialog()(sessionRecap.ts)只保留有非空文本且不是 thoughtuser/model部分,原因有二:

  • 工具调用 / 响应:单个functionResponse可达 10K+ token,30 条这样的消息会让 recap LLM 淹没在无关细节中——既浪费 token,又会让 recap 偏向"调用了 X 工具去读 Y 文件"这类实现噪音;
  • thought 部分:承载模型的内部推理。纳入其中可能把隐藏的思维链当作对话,并在 recap 文本中将其表面化。

此外,该函数还调用了getStartupContextLength(history)切掉启动上下文,并对每条文本执行stripSystemReminderBlocks()剥除系统提醒块(如STARTUP_SKILL_LISTADDED_MCP_TOOLSPLAN_MODE_REMINDERIDE_CONTEXT等)。这一点有单元测试直接验证:sessionRecap.test.ts 断言最终的序列化输入中不包含任何系统提醒块内容,同时保留真实用户消息。

在丢弃空消息之后,takeRecentDialog()(sessionRecap.ts)切片到最近 30 条消息(RECENT_MESSAGE_WINDOW = 30),并且拒绝让切片起始于悬空的 model/tool 响应——即向后推进到第一条user角色消息为止,保证回合结构完整。

并发与边界情况

自动触发 hook 状态机

useAwaySummary维护三个 ref(useAwaySummary.ts):

Ref含义
blurredAtRef失焦开始时间(在焦点回归前不清除)
recapPendingRef是否有一次 LLM 调用在途
inFlightRef当前在途的AbortController

useEffect依赖数组为[enabled, config, isFocused, isIdle, addItem, thresholdMs],各事件的处理逻辑:

事件动作
!enabled \|\| !config中止在途调用 + 清空inFlightRef+ 清空blurredAtRef
!isFocusedblurredAtRef === null设置blurredAtRef = Date.now()
isFocusedblurredAtRef === null直接返回(无失焦周期可处理——首次渲染或短暂失焦重置之后)
isFocused且失焦时长 < 5 分钟清空blurredAtRef,等待下一个失焦周期
isFocused且失焦 ≥ 5 分钟且recapPendingRef返回(去重)
isFocused且失焦 ≥ 5 分钟且!isIdle保留blurredAtRef,等待本轮结束(isIdle在依赖中,流式完成后 effect 会重新触发)
isFocused且失焦 ≥ 5 分钟且shouldFireRecap返回 false清空blurredAtRef并返回——会话自上次 recap 以来没有足够的新进展(要求 ≥ 2 个用户回合,复刻 Claude Code)
isFocused且所有条件满足清空blurredAtRef、置recapPendingRef = true、创建AbortController、发起 LLM 请求

.then回调会重新检查isIdleRef.current:如果用户在 LLM 运行期间已经开始新一轮对话,迟到的 recap 会被丢弃,避免在回合中途插入。.finally清空recapPendingRef,并且仅当inFlightRef.current === controller时才清空inFlightRef(避免覆盖更新的 controller)。另有一个独立的useEffect在卸载时中止在途 controller。

值得展开的细节:history通过historyRef触发时读取,而不是加入 effect 依赖——这样历史变化不会导致每次消息都重新评估(useAwaySummary.ts)。

去重门控(Dedup gates)

shouldFireRecap()(useAwaySummary.ts)实现与 Claude CodeSc1/Rc1/Ic1对齐的门控:

  • MIN_USER_MESSAGES_TO_FIRE = 3:至少要有 3 条用户回合总数(sentToModel !== false才算,steer 消息不计入);
  • MIN_USER_MESSAGES_SINCE_LAST_RECAP = 2:若历史中已有 recap,则自上次 recap 之后至少需要 2 条新用户回合才能再次触发。

后者防止用户两次短暂 alt-tab 且中间没有做任何新工作时,产生背靠背的重复 recap。这两个门控都有测试覆盖(useAwaySummary.test.ts):例如 2 条真实用户消息 + 1 条 steer 消息时(总计 3 条但只有 2 条真实回合),不会触发调用;历史中已有 recap 且其后只有 1 条新用户回合时,同样不会触发。

自动 recap 的持久化

一个容易被忽略但很关键的行为:自动触发的 recap 会通过config.getChatRecordingService()?.recordSlashCommand({ phase: 'result', rawCommand: '/recap', outputHistoryItems: [...] })记录(useAwaySummary.ts),镜像手动/recap由斜杠命令处理器执行的记录方式,从而保证自动 recap 在/resume后也能存活。只记录result阶段——若记录invocation阶段,会在恢复时重放一行伪造的> /recap用户行。该行为同样有测试覆盖(useAwaySummary.test.ts)。

/recap门控

CommandContext.ui.isIdleRef暴露当前流状态(镜像已有的btwAbortControllerRef模式)。在交互模式下,recapCommand!isIdleRef.currentpendingItem !== null时拒绝执行(recapCommand.ts),返回错误消息"另一个操作正在进行中"。仅判断pendingItem是不够的,因为正常的模型回复运行在streamingState === RespondingpendingItem === null的状态下。

其他行为细节:/recap在无配置时返回错误消息;非交互模式下,recap 结果作为messageType: 'info'的消息返回而不是插入历史项;若 recap 为null,交互模式返回信息提示"对话上下文还不足以生成 recap"(recapCommand.ts)。

配置与模型选择

用户可配置项

设置默认值说明
general.showSessionRecapfalse仅影响自动触发。手动/recap忽略此设置。
general.sessionRecapAwayThresholdMinutes5失焦多少分钟后,在焦点回归时触发自动 recap。与 Claude Code 默认值一致。
fastModel未设置推荐设置(如qwen3-coder-flash),以获得又快又便宜的 recap。

这些设置在 settingsSchema.ts 中注册为showInDialog: true的常规项(requiresRestart: false,可在设置对话框直接修改)。sessionRecapAwayThresholdMinutes还声明了minimum: 1,而useAwaySummary侧对非正值会回退到 5 分钟默认值(DEFAULT_AWAY_THRESHOLD_MINUTES = 5,见 useAwaySummary.ts)。

showSessionRecap默认关闭的理由在源码注释中写得很清楚(settingsSchema.ts):环境性的后台 LLM 调用不应在用户不知情的情况下被默默开启,尤其当fastModel未设置时,调用会落在主编码模型上。手动/recap不受此限制。

模型回退

config.getFastModel() ?? config.getModel()

  • 用户设置了fastModel且对当前认证类型有效 → 使用fastModel
  • 否则 → 回退到主会话模型(可用,只是更贵更慢)。

可观测性

createDebugLogger('SESSION_RECAP')输出调试日志(sessionRecap.ts),包括:

  • recap 路径捕获的异常(debugLogger.warn,如"Recap generation failed: ...");
  • 各类跳过原因的debugLogger.debug信息:无 LLM 客户端、历史过短(少于 2 条消息)、过滤后无对话消息、模型返回空文本、标签提取失败、被信号中止等。

所有失败对用户完全透明——recap 是辅助功能,绝不向 UI 抛错。开发者可在调试日志文件中按[SESSION_RECAP]标签 grep:日志默认写入~/.qwen/debug/<sessionId>.txtlatest.txt符号链接指向当前会话);通过QWEN_DEBUG_LOG_FILE=0禁用。

明确排除的范围(Out of Scope)

原因
/recap的进度 UI(spinner / pendingItem)3–5 秒等待可以接受;增加复杂度不值得。
自动化测试服务较小(约 150 行),先手动端到端验证;单元测试可单独 PR 落地(注:仓库中已存在 sessionRecap.test.ts 与 useAwaySummary.test.ts,覆盖系统提醒剥离与去重门控等关键路径)。
本地化提示词system prompt 是给模型的;英文是最可靠的基底。输出语言由模型根据对话自行选择。
QWEN_CODE_ENABLE_AWAY_SUMMARY环境变量Claude Code 用它来在遥测关闭时保持功能开启;Qwen Code 当前的遥测模型不需要这个开关。
/resume完成后的自动 recap是自然的后续演进,但需要在useResumeCommand中寻找挂载点;超出本 PR 范围。

实战速查

在交互终端中:

# 手动生成一次 recap(不受 showSessionRecap 限制) /recap # 开启自动 recap(写入 settings.json 或通过设置对话框) # general.showSessionRecap: true # general.sessionRecapAwayThresholdMinutes: 5 # 可调,如 10

通过 Daemon HTTP / TypeScript SDK:

import { DaemonClient } from 'qwen-code-sdk'; const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:PORT' }); // 预检能力 const caps = await client.capabilities(); if (caps.features.session_recap) { const { recap } = await client.recapSession(sessionId); // recap 可能是 null(历史过短或模型瞬时失败),属正常契约 }

排查问题:

# 在调试日志中检索 recap 相关记录 grep 'SESSION_RECAP' ~/.qwen/debug/latest.txt

小结

Session Recap 是 Qwen Code 中一个"小而完整"的功能切片:三条触发路径收敛到单一核心服务函数,提示词与标签提取保证了输出的形状可控,历史过滤在 token 成本与信息相关性之间取得平衡,hook 状态机与去重门控覆盖了失焦、跨回合、重复触发等全部边界,而"最佳努力"原则通过贯穿 UI 层、服务层与 daemon 层的空值返回与静默失败得到严格贯彻。理解这套设计,不仅有助于熟练使用/recap与自动摘要能力,也能为在 Qwen Code 中接入其他"旁路查询"型 AI 能力(side-query)提供一份可参考的实现范式。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 23:37:57

嵌入式AI编程:重构STM32开发流程的五层耦合方法

1. 这不是“用AI写代码”&#xff0c;而是重构嵌入式开发的认知框架 你搜“AI编程 STM32”&#xff0c;刷出来的大多是“让ChatGPT帮你生成GPIO初始化代码”这类短视频标题。但真正跑通一个带AI能力的STM32项目&#xff0c;比如用本地模型做实时语音关键词唤醒、用轻量级Transf…

作者头像 李华
网站建设 2026/9/13 23:37:55

小白也能看懂:AI大模型何时调用RAG?5大场景+收藏必备指南

本文详细解析了AI大模型在何种场景下会使用RAG技术&#xff0c;包括知识时效性断层、私有数据需求、高事实性要求、长尾查询和动态更新需求。通过“全能助理”的生动比喻&#xff0c;将复杂概念转化为通俗易懂的实例&#xff0c;帮助读者快速理解RAG的作用和限制&#xff0c;是…

作者头像 李华
网站建设 2026/9/13 23:37:53

GD32H759 + RT-Thread 工控实战--第1篇 CAN总线

前言 本篇开始调试GD32H7的CAN&#xff0c;目标为跑通回环、自发收、以及与上位机通讯。需要额外2条杜邦线、一块candlelight USB-CAN模块。本篇为边开发边记载&#xff0c;所以阅读顺序可能不那么愉快。 一、驱动代码编写与kconfig修改 1.1 为何要自己编写can驱动 首先rt-t…

作者头像 李华