Files
smartmate/docs/frontend/newagent_thinking_summary_对接说明.md
Losita f81f137791 Version: 0.9.53.dev.260429
后端:
1. 流式思考链路从 raw reasoning_content 切到 `thinking_summary` 摘要协议,补齐摘要 prompt、digestor 与 Lite 压缩链路,plan / execute / fallback 统一改为“只出摘要、不透原始推理”,正文开始后自动关停摘要流。
2. thinking_summary 打通 timeline / SSE / outbox 持久化闭环,只落 detail_summary 与必要 metadata,并补强 seq 自检、冲突幂等识别与补 seq 回填,提升重放恢复稳定性。
3. 会话历史口径继续收紧,assistant 正文与时间线不再回写 raw reasoning_content,仅保留正文与思考耗时,避免刷新恢复时再次暴露内部推理文本。

前端:
4. 助手页开始接入 thinking_summary 实时流与历史恢复,补齐短摘要状态、长摘要折叠区、正文开流后自动收口,并增加调试入口用于协议联调与验收。
5. 当前前端助手页仍是残次过渡态,本版先以 thinking_summary 协议接通和基础渲染为主,样式、交互与细节体验暂未收平,下一版集中修复。

仓库:
6. 补充 thinking_summary 对接说明,明确 SSE 协议、timeline 恢复口径与 short/detail summary 的使用边界。
2026-04-29 01:00:38 +08:00

390 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NewAgent 思考摘要前端对接说明
## 背景
后端已经不再把模型原始 `reasoning_content` 直接透传给前端。新的展示入口是 SSE 顶层 `extra.kind = "thinking_summary"` 事件。
目标体验:
- 用户等待模型深度思考时,前端每隔几秒收到一条短摘要,作为当前思考状态的轻量提示。
- 展开后展示稍长的 `detail_summary`,多条按时间追加。
- 模型开始输出正文后,当前思考摘要停止更新。
- 刷新会话后,只恢复长摘要,不恢复短摘要。
## 实时 SSE 协议
聊天接口仍然是:
```http
POST /api/v1/agent/chat
Content-Type: application/json
Accept: text/event-stream
```
SSE 每个业务包仍是标准格式:
```text
data: {json}
data: [DONE]
```
后端保活心跳是 SSE 注释行:
```text
: ping
```
前端按现有逻辑忽略不能 JSON.parse 的块即可。
## thinking_summary 事件
实时思考摘要事件没有 `delta.content`,也没有 `delta.reasoning_content`。前端应从顶层 `extra.thinking_summary` 读取。
示例:
```json
{
"id": "trace-id",
"object": "chat.completion.chunk",
"created": 1777399000,
"model": "pro",
"extra": {
"kind": "thinking_summary",
"block_id": "plan.speak",
"stage": "plan",
"display_mode": "append",
"thinking_summary": {
"summary_seq": 1,
"short_summary": "正在梳理计划",
"detail_summary": "正在把用户目标拆成可执行步骤,并检查是否需要补充约束。",
"duration_seconds": 3.214
}
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `extra.kind` | 固定为 `thinking_summary`。 |
| `extra.block_id` | 当前摘要所属展示块,例如 `plan.speak``execute.speak``fallback.speak`。建议作为分组 key 的一部分。 |
| `extra.stage` | 当前节点阶段,例如 `plan``execute``fallback`。 |
| `extra.display_mode` | 当前固定为 `append`,表示长摘要按条追加。 |
| `thinking_summary.summary_seq` | 同一个摘要器内递增,用于忽略重复或乱序摘要。不要当作全局 timeline seq。 |
| `thinking_summary.short_summary` | 实时短摘要,只用于当前流式展示,不持久化。 |
| `thinking_summary.detail_summary` | 展开态长摘要,按 append 语义追加;刷新后也只恢复这个字段。 |
| `thinking_summary.duration_seconds` | 从首次收到 reasoning 到生成该摘要的耗时秒数,可能是小数。 |
| `thinking_summary.final` | 可选。若出现 `true`,表示该摘要器在没有正文打断的情况下自然收口。不要依赖它一定出现。 |
已删除字段:
- `state` 已从协议、prompt、timeline 持久化里删除,前端不要再依赖或展示。
## 前端处理建议
建议把思考摘要作为 assistant 消息内的一个子结构,而不是普通正文。
推荐 key
```ts
const key = extra.block_id || extra.stage || 'thinking'
```
推荐类型:
```ts
export interface ThinkingSummaryPayload {
summary_seq?: number
short_summary?: string
detail_summary?: string
final?: boolean
duration_seconds?: number
}
export interface ThinkingSummaryBlock {
key: string
stage?: string
blockId?: string
latestSeq: number
latestShort: string
details: Array<{
seq: number
text: string
durationSeconds?: number
final?: boolean
}>
active: boolean
collapsed: boolean
}
```
实时处理伪代码:
```ts
function handleThinkingSummary(extra: StreamExtra, message: AssistantMessage) {
if (extra.kind !== 'thinking_summary') return false
const summary = extra.thinking_summary
if (!summary) return true
const key = extra.block_id || extra.stage || 'thinking'
const block = ensureThinkingSummaryBlock(message, key, {
stage: extra.stage,
blockId: extra.block_id,
})
const seq = summary.summary_seq ?? block.latestSeq + 1
if (seq <= block.latestSeq) return true
block.latestSeq = seq
block.active = summary.final !== true
if (summary.short_summary?.trim()) {
block.latestShort = summary.short_summary.trim()
}
if (summary.detail_summary?.trim()) {
block.details.push({
seq,
text: summary.detail_summary.trim(),
durationSeconds: summary.duration_seconds,
final: summary.final,
})
}
return true
}
```
正文开始时的处理:
```ts
function handleAssistantContentStart(message: AssistantMessage) {
// 后端正文一出现就会停止当前 block 的摘要;
// 前端这里也可以把活跃思考块收口,避免动效继续闪。
message.thinkingSummaryBlocks?.forEach(block => {
block.active = false
})
}
```
注意:
- 收到 `thinking_summary` 时,不要追加到 `assistantMessage.content`
- 收到 `thinking_summary` 时,不要写入旧的 `assistantMessage.reasoning`
- 若仍收到旧链路 `delta.reasoning_content`,可以保留兼容,但新样式应优先使用 `thinking_summary`
- `summary_seq` 只在同一个 `block_id/stage` 下去重;不同 block 不要互相比较。
## 展示语义
短摘要:
- 展示最新一条 `short_summary`
- 适合放在折叠态标题、胶囊、加载条旁边。
- 不要持久化到本地历史,也不要在刷新恢复后强行补出来。
长摘要:
- 每次收到非空 `detail_summary` 就追加一条。
- 展开态展示 `details` 列表。
- 如果你想做得更像 Gemini/豆包,可以折叠态只露最新短摘要,展开态按时间展示长摘要列表。
收口条件:
- 收到第一段 `delta.content`:关闭当前 assistant 消息里的活跃思考态。
- 收到 `finish_reason``[DONE]`:关闭所有活跃思考态。
- 收到 `thinking_summary.final === true`:可以关闭对应 block但不要依赖它总会出现。
## 历史 timeline 恢复
刷新会话时读取:
```http
GET /api/v1/agent/conversation-timeline?conversation_id={conversation_id}
```
统一响应仍是:
```json
{
"status": "0",
"info": "success",
"data": []
}
```
`thinking_summary` timeline item 示例:
```json
{
"id": 123,
"seq": 8,
"kind": "thinking_summary",
"content": "正在把用户目标拆成可执行步骤,并检查是否需要补充约束。",
"payload": {
"stage": "plan",
"block_id": "plan.speak",
"display_mode": "append",
"summary_seq": 1,
"detail_summary": "正在把用户目标拆成可执行步骤,并检查是否需要补充约束。",
"duration_seconds": 3.214
},
"created_at": "2026-04-28T21:00:00+08:00"
}
```
历史恢复规则:
- 只恢复 `detail_summary`,没有 `short_summary`
- 按 timeline item 的 `seq` 排序渲染即可,后端已升序返回。
- 可用 `payload.block_id || payload.stage || "thinking"` 归组到对应 assistant 消息附近。
- 如果当前前端还没做跨事件归组,可以先把它渲染为 assistant 消息里的“思考摘要条目”,位置按 timeline 顺序插入。
建议更新现有前端类型:
```ts
export interface TimelineThinkingSummaryPayload {
stage?: string
block_id?: string
display_mode?: 'append'
summary_seq?: number
detail_summary?: string
duration_seconds?: number
final?: boolean
}
export interface TimelineEvent {
id: number
seq: number
kind:
| 'user_text'
| 'assistant_text'
| 'tool_call'
| 'tool_result'
| 'confirm_request'
| 'schedule_completed'
| 'business_card'
| 'thinking_summary'
role?: 'user' | 'assistant'
content?: string
payload?: {
stage?: string
block_id?: string
display_mode?: 'append' | 'replace' | 'card'
thinking_summary?: never
detail_summary?: string
summary_seq?: number
duration_seconds?: number
final?: boolean
tool?: TimelineToolPayload
confirm?: TimelineConfirmPayload
business_card?: TimelineBusinessCardPayload
}
tokens_consumed?: number
created_at?: string
}
```
## 与正文/工具卡片的关系
同一轮流里可能出现:
1. `thinking_summary`
2. `tool_call` / `tool_result`
3. `assistant_text``delta.content`
4. `finish`
5. `[DONE]`
前端建议:
- `thinking_summary` 是“等待过程”组件。
- `tool_call` / `tool_result` 继续走现有工具卡片。
- `delta.content` 继续追加到 assistant 正文。
- `finish` / `[DONE]` 只负责收尾,不需要生成可见消息。
## 测试用例
### 1. 只有摘要,还没正文
输入事件:
```json
{
"extra": {
"kind": "thinking_summary",
"block_id": "plan.speak",
"stage": "plan",
"display_mode": "append",
"thinking_summary": {
"summary_seq": 1,
"short_summary": "正在理解需求",
"detail_summary": "正在识别用户的目标、约束和需要补充的信息。",
"duration_seconds": 2.1
}
}
}
```
预期:
- 折叠态显示“正在理解需求”。
- 展开态新增一条 detail。
- 正文区域不新增文字。
### 2. 多条摘要追加
输入 `summary_seq=1,2,3`
预期:
- `latestShort` 使用第 3 条短摘要。
- `details` 有 3 条,按收到顺序或 seq 升序展示。
### 3. 乱序或重复摘要
已处理到 `summary_seq=3` 后,又收到 `summary_seq=2`
预期:
- 忽略旧事件,不回退短摘要,不追加 detail。
### 4. 正文开始
收到:
```json
{
"choices": [
{
"delta": { "content": "我整理好了,下面是建议:" }
}
]
}
```
预期:
- 当前活跃思考块停止 loading 动效。
- 正文正常追加。
- 后续若仍意外收到同 block 摘要,可按 seq 处理,但 UI 上建议不再重新激活。
### 5. 历史恢复
timeline 返回 `kind=thinking_summary`
预期:
- 只展示 `payload.detail_summary || content`
- 不展示短摘要占位。
- 不需要显示 `state`,协议里已经没有这个字段。
## 最小改动清单
1. `StreamEventPayload.extra` 增加 `thinking_summary` 字段。
2. `TimelineEvent.kind` 增加 `thinking_summary`
3. SSE 解析里在 `handleStreamExtraEvent` 增加 `extra.kind === "thinking_summary"` 分支。
4. 收到正文 `delta.content` 时,把当前思考摘要块置为非活跃。
5. 历史 timeline 恢复时支持 `kind === "thinking_summary"`,只恢复长摘要。