案例 #1773:Agent thinking 开关与 RAG 时间线
对齐标杆 case-1828 · 写作标准
PR:#1773 | 9 文件 +256/-34 | 2026-06-23 合入 | lyingbug
结论先行
Quick-answer Agent 支持模型 thinking / reasoning 能力,但三处断裂导致体验与协议不一致:(1)config.thinking 未设置时为 nil,带 thinking_control: thinking_type 的模型 收不到明确的 disabled,wire 上字段被省略;(2)后端缺少 resolved thinking 的 pipeline 日志,难 debug;(3)前端 RagPipelineProgress 在 thinking 关闭 时仍 闪一下「思考」行,开启时又 未在时间线内展示 reasoning SSE。
修复:EnsureDefaults 把 nil 钉为 explicit false;prepareChatModel / applyAgentOverrides 打日志;RagPipelineProgress.vue 重构 thinking 行与 tree-child-last 逻辑;Agent 编辑器把 thinking: null 规范为 false。
适合谁读:改 Agent 配置、QA 管线、RAG 可观测 UI(SSE 时间线)的全栈贡献者。
与 #1248:间接 — feedback 气泡与时间线同属 chat UX;本 PR 的「nil → 明确默认值」同 case-1633 的字段归一化。
链 Ch45 Agent/Token · Ch12 Agentic RAG · 对比 case-1772(QA 入口权限)。
1. 问题现象(What)
1.1 后端 / 协议
| 配置 | 修复前 wire | 模型行为 |
|---|---|---|
Agent 未显式设 thinking(nil) |
可能 省略 thinking 字段 |
thinking_type 控制模型 不确定 enabled/disabled |
Agent thinking: true |
应有 thinking.type=enabled |
依赖 override 链正确 |
PR test plan:builtin quick-answer agent 开 thinking 保存后发消息 → LLM 请求应含 "thinking": {"type": "enabled"},日志 Using custom agent's thinking: true。
1.2 前端 / 时间线
| 状态 | 修复前 UX | 修复后 UX |
|---|---|---|
| thinking 关(默认) | 答案前 短暂闪现「思考」行 | 时间线:query understand → answer/done,无空思考 |
thinking 开 + 模型流式 reasoning_content |
reasoning 未进 RagPipelineProgress 树 | 时间线内 thinking 行,答案开始后折叠 |
1.3 复现(Test plan 摘要)
- Agent 编辑器开启 thinking → 保存 → 发 quick-answer 消息 → 查 LLM 请求体 + 后端 log
- thinking 默认关 → 发消息 → 不应 闪「思考」
- thinking 开 + 支持 reasoning 的模型 → 时间线出现 thinking 行,答案出来后折叠
go test ./internal/types/ -run TestEnsureDefaults_Thinkingnode --test frontend/.../RagPipelineProgress.style.test.mjs
2. 定位步骤
# 后端:thinking 默认值与 override
rg "Thinking|thinking" internal/types/custom_agent.go
rg "SummaryConfig.Thinking|applyAgentOverrides" internal/application/service/
# 前端:RAG 时间线
rg "showThinkingStep|RagPipelineProgress|reasoning" frontend/src/views/chat/
阅读顺序:
internal/types/custom_agent.go—EnsureDefaultsinternal/types/custom_agent_test.go— 行为契约session_qa_helpers.go— override 到ChatManagechat_pipeline/common.go—prepareChatModel日志RagPipelineProgress.vue— UI 树与showThinkingStep
3. 根因分析(Why)
3.1 类比
遥控器上「静音」键若是 三态(开/关/未定义),电视可能猜错;应 强制两态(开/关),未配置 = 关。thinking: nil 就是「未定义」,对 thinking_type 模型却是 协议级歧义。
3.2 数据流(配置 → wire → UI)
flowchart TD
A[Agent 配置 JSON<br/>thinking: null/true/false] --> B[EnsureDefaults]
B --> C{thinking == nil?}
C -->|修复前| D[nil 传入 SummaryConfig]
C -->|修复后| E[explicit false 指针]
D --> F[prepareChatModel<br/>可能省略 thinking 字段]
E --> G[wire: disabled/enabled 明确]
G --> H[LLM API]
H --> I[SSE: reasoning_content / thinking events]
I --> J[RagPipelineProgress.vue]
J --> K{showThinkingStep 逻辑}
K --> L[修复闪动 + 内嵌 reasoning 行]
3.3 根因分层
| 层 | 根因 |
|---|---|
| types | EnsureDefaults 未 pin Thinking,JSON null 与「未传」难区分 |
| service | override 后无 structured log,线上只能猜 thinking 最终值 |
| Vue | tree-child-last / showActivityIndicator 与 thinking 行互斥条件错误 → 关 thinking 仍闪 UI |
4. 解决方案(How)
4.1 后端:EnsureDefaults 钉 explicit false
// internal/types/custom_agent.go
if a.Config.Thinking == nil {
disabled := false
a.Config.Thinking = &disabled
}
为何不用「omit 表示 false」? — 部分 provider 的 thinking_control: thinking_type 要求显式 type;omit ≠ disabled。
4.2 单测契约
// custom_agent_test.go
func TestEnsureDefaults_ThinkingExplicitFalse(t *testing.T) { /* nil → *false */ }
func TestEnsureDefaults_ThinkingPreservesTrue(t *testing.T) { /* true 不被覆盖 */ }
4.3 Pipeline 日志
// chat_pipeline/common.go — prepareChatModel
if opt.Thinking != nil {
pipelineInfo(ctx, "Stream", "thinking_option", map[string]interface{}{
"enabled": *opt.Thinking,
})
}
session_qa_helpers.go:override 后 log Using custom agent's thinking: %v;若仍为 nil 则 Warn(防御 EnsureDefaults 遗漏)。
4.4 前端:RagPipelineProgress
- 新增
showThinkingStep— 仅在 thinking SSE / reasoning 存在时显示思考行 - 修正
tree-child-last:引用块、thinking 行、done 行 互斥顺序(PR 改多组:class) ref="rootElement"— 配合样式/滚动测试AgentEditorModal.vue:加载配置时thinking: null→false
4.5 逐文件改动表
| 文件 | ± | 说明 |
|---|---|---|
internal/types/custom_agent.go |
+6 | EnsureDefaults pin false |
internal/types/custom_agent_test.go |
+23 | NEW 测试 |
chat_pipeline/common.go |
+5 | thinking_option 日志 |
session_qa_helpers.go |
+5/-1 | override 注释 + warn |
RagPipelineProgress.vue |
+177/-26 | 时间线 thinking UX |
RagPipelineProgress.style.test.mjs |
+27/-4 | 前端回归 |
AgentStreamDisplay.vue |
+2/-3 | 流展示微调 |
AgentStreamDisplay.style.test.mjs |
+8 | 测试 |
AgentEditorModal.vue |
+3 | null → false |
4.6 验证命令
go test ./internal/types/ -run TestEnsureDefaults_Thinking -v
node --test frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs
5. Review 与合入
PR 四段式
| 段 | 内容 |
|---|---|
| What | thinking nil 导致 wire/UI 不一致;时间线闪动 |
| Why | 缺 explicit default + 前端树条件错误 |
| How | EnsureDefaults + 日志 + RagPipelineProgress + 编辑器归一化 |
| Test | go test + node –test + 手动 quick-answer 三条路径 |
维护者 checklist
EnsureDefaults是否 不覆盖 显式true?- thinking 关时 UI 零思考行?
- thinking 开时 reasoning 在 时间线内且答案后折叠?
- Agent 编辑器保存后再加载,
thinking非 null?
6. 与 RAG 管线
| 环节 | 本 PR 位置 |
|---|---|
| Retrieve / Generate 之间 | Quick-answer RagPipelineProgress 展示检索/理解步骤 |
| LLM 请求 | SummaryConfig.Thinking → provider wire |
| 可观测 | pipeline log thinking_option — 对标 eval/debug Ch45 |
7. 可复用模式
- 三态 JSON → 两态业务:
nil在 API/DB 常见,上 wire 前 EnsureDefaults 钉默认值 - 后端 log + 前端时间线同 PR — Agent 可观测 bug 常需 全栈 才闭环
.style.test.mjs— UI 树结构用 node –test 锁回归,不只手动点 UI
8. 常见误区
错误认知:omit thinking = 关闭 thinking。
正确:thinking_type 模型可能 omit ≠ disabled — 必须 explicit。
错误认知:只改前端闪动就够了。
正确:wire 仍歧义时,UI 怎么改都不稳定。
错误认知:EnsureDefaults 可以 overwrite true。
正确:单测 PreservesTrue — 只填 nil。
错误认知:Agent PR 只改 qa.go。
正确:配置在 types → session override → pipeline → Vue,四层要对齐。
9. 思考点
- 若新增
config.reasoning_effort枚举,应放在 EnsureDefaults 还是 provider adapter? - case-1772 删 dead RBAC;本 PR 补默认值 — 何时删 vs 补?
- 设计 RagPipelineProgress 测试:thinking 关时 DOM 里不应出现哪几个 class?
- #1248 feedback 按钮与时间线 并存 时,
tree-child-last规则如何扩展?
章末自检
- 解释
nilthinking 对thinking_type模型的 wire 影响 - 写出
go test与node --test命令 - 说清 thinking 关 vs 开时时间线各应有哪些行