fix(backthink): 后台思考静默广播——不再把内部推理泄到聊天思维链 + XML 动作/旁白解析
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

【静默广播修复(bug:思维链露出 backthink 推理)】
- 根因:LoopThinker.Think 复用同一 loop.Loop 且 loop 全局带 OnStream,backthink 后台思考时
  把 [background autonomous reflection] 的 thinking/流式事件广播到前端,被当聊天思维链显示。
- loop/mute_ctx.go:WithMutedBroadcast(ctx) + isMutedBroadcast(ctx)——context 上静默标志。
- loop 四处广播抑制(stream.go 流式 / step.go 静默走非流式 agentStep / request.go capacity 事件 /
  tools.go tool_execution),静默时跳过 UI 广播,推理照常返回结论。
- thinker.go LoopThinker.Think 用 loop.WithMutedBroadcast(ctx),backthink 不再泄 thinking。

【XML 动作/旁白结构化解析】
- cyrene.yaml systemPrompt 加「表达方式:动作与旁白」——动作 <action>…</action>、
  旁白 <narration>…</narration>;克制≤1-2个/段、贴合唤醒态、重要信息不加、朗读正文忽略标签。
- 前端 parseContent.ts:ContentPart[](text/action/narration),非贪婪+反向引用、跨行、非法/
  未闭合兜底纯文本不崩;summarizeContent 供思维导图/状态。
- 前端 renderContent.tsx:action/narration → 灰斜体(mu-text-lo italic)句中原位渲染。
- 应用到 chat.tsx UserBubble/AssistantBubble + messages 各 mode body。

go build/vet/test(loop/backthink)绿 + 前端 tsc/vite 绿。
This commit is contained in:
Aska committed 2026-09-06 22:09:51 +08:00
1 parent 6dbb4a2e18
commit 33eeadfc38
11 files changed
+146 -19

No files matched your search

+6 -5
View File
@@ -275,14 +275,14 @@ func (l *Loop) invokeOne(ctx context.Context, d *tool.Dispatcher, tc agent.ToolC
if d == nil {
return ToolOutcome{Err: fmt.Errorf("no tool dispatcher configured for %q", tc.Name), ErrKind: tool.KindExecution}
}
l.emitToolExecution(tc.Name, "start")
l.emitToolExecution(ctx, tc.Name, "start")
_, toolSpan := l.span(ctx, "tool."+tc.Name, trace.SpanKindTool)
result, err := d.Invoke(ctx, tc.Name, tc.Args)
phase := "end"
if err != nil {
phase = "end:error"
}
l.emitToolExecution(tc.Name, phase)
l.emitToolExecution(ctx, tc.Name, phase)
out := ToolOutcome{Result: result, Err: err, ErrKind: errorKind(err)}
if err != nil {
l.spanProp(toolSpan, "error", err.Error())
@@ -297,9 +297,10 @@ func (l *Loop) invokeOne(ctx context.Context, d *tool.Dispatcher, tc agent.ToolC
// emitToolExecution forwards a tool_execution_update progress event to the
// frontend when the loop is on the streaming path (OnStream configured); it is a
// no-op otherwise. phase is a short label like "start" or "end:error".
func (l *Loop) emitToolExecution(name, phase string) {
if l.onStream == nil {
// no-op otherwise (muted internal passes skip it too). phase is a short label
// like "start" or "end:error".
func (l *Loop) emitToolExecution(ctx context.Context, name, phase string) {
if l.onStream == nil || isMutedBroadcast(ctx) {
return
}
l.onStream(model.StreamEvent{Kind: model.StreamKindToolExecution, Delta: phase + ":" + name})