Skip to content

fix: 抗截断模式适配 agent 工具调用场景,恢复流式输出并保留 finish_reason 收尾块 - #415

Open
Theater-ahyeon wants to merge 2 commits into
su-kaka:masterfrom
Theater-ahyeon:fix/anti-truncation-agent-streaming
Open

Theater-ahyeon wants to merge 2 commits into
su-kaka:masterfrom
Theater-ahyeon:fix/anti-truncation-agent-streaming

Conversation

@Theater-ahyeon

Copy link
Copy Markdown

修复抗截断模式与 agent 类客户端配合时的三个问题

背景

在本地用 pi coding agent 通过 抗截断/gemini-3.8-flash-high(Antigravity 端点)做编码任务时,发现抗截断流式处理器对"模型调用客户端真实工具"和"流式输出"这两种场景处理有问题。逐层排查后定位到 AntiTruncationStreamProcessor.process_stream() 的三个缺陷,本 PR 一并修复。

问题与修复

1. 真实工具调用回合被误续传 → 同一工具调用重复下发 N 次

处理器只把 emit_answer 视为完整回答;当模型调用的是客户端自己的工具(agent 场景的常态)时,会被判定为"回答被截断"而重发请求,直到 max_attempts 用尽。结果是客户端在一条回复里收到 max_attempts 个一模一样的工具调用,会话直接卡死。

修复:本轮只要出现了真实工具调用(has_real_tool_calls),即视为完整助手回合,立即收尾(流式与非流式路径均已处理)。

2. 正文被整体缓冲 → 长时间零输出 + 无意义续传

普通文本原本先攒在 side buffer、等整个流结束才一次性发出(防拼接设计),且只要没等到 emit_answer 就续传重试。客户端的体验是:生成期间零输出,一次回答要静默等待 1~3 倍生成时间。

修复:

  • 正文、思考、工具调用全部实时透传(思考分块由路由层转换为 reasoning_content),恢复流式体验;
  • 续传仅保留给"整个流零产出"的回合(如上游只回思考就结束的截断场景)——这也是抗截断本来要解决的问题;
  • 零产出续传时从预填充模式退回提示词模式:空预填充等于原样重发同一请求,提示词模式能明确要求模型给出回答。

3. 上游收尾块被丢弃 → 客户端误判流异常并重试 → 回答重复输出

实测上游在 emit_answer 回合后发送 finishReason=STOP 的收尾块,但该块带有一个空的 text: "" part,被 _chunk_has_plain_text("text" in part)判定为正文而丢弃。结果整个流没有任何块携带非空 finish_reason。

严格校验的客户端(pi 实测)会直接抛出 Stream ended without finish_reason 并按错误自动重试——表现为同一条回答被重复输出多次,且每条消息 stopReason: "error"。

修复:只把非空正文文本视为产出(part.get("text") 真值判断),空的 text: "" part 不再触发丢弃,收尾块(finish_reason + usage)正常透传。

验证(本地实测,Antigravity 凭证 / Gemini 3.8 Flash)

场景 修复前 修复后
纯聊天 正文一次性下发(无流式),无 finish_reason 思考 → 正文 → finish_reason:"stop" + usage → [DONE]
带工具定义 同一工具调用重复 3 次 1 次工具调用 → finish_reason:"tool_calls" + usage → [DONE]
多轮回路(工具结果回传) — 正文 → "stop" + usage → [DONE]

抓取上游原始 SSE 确认:emit_answer 回合的上游流为 fnCall(emit_answer) → finishReason=STOP, parts=[text[0]](空文本 part),修复后收尾块可正常到达客户端。

行为变化说明(供审阅参考)

  • "同一回合先输出文本、后又出现 emit_answer" 的防拼接场景:由于文本已实时透传,无法再撤回,现在会打出警告日志并可能造成内容重复。实测中模型要么走 emit_answer、要么直接输出正文,两者混现较为少见;权衡下优先保证流式体验。
  • 纯聊天场景(模型按注入指令走 emit_answer)的行为不受影响。

修复抗截断(抗截断/ 前缀模型)在与编码智能体类客户端(如 pi)配合时的三个问题:

1. 真实工具调用回合被误续传:处理器只认 emit_answer 为完整回答,模型
   调用客户端真实工具时会被当作"回答被截断"而重发请求,导致同一工具
   调用在一条回复中被重复下发 max_attempts 次。现在只要本轮出现真实
   工具调用,即视为完整助手回合并立即收尾(流式与非流式路径均已处理)。

2. 正文被整体缓冲导致无流式输出:普通文本原本先攒在 side buffer、等流
   结束才一次性发出,且只要未等到 emit_answer 就续传重试,客户端要静默
   等待 1~3 倍生成时间。现在正文/思考/工具调用全部实时透传(思考分块由
   路由层转换为 reasoning_content),续传仅保留给"整个流零产出"(如上游
   只回思考就结束)的回合;零产出续传时从预填充模式退回提示词模式
   (空预填充等于原样重发)。

3. 上游收尾块被丢弃导致客户端误判流异常:实测上游在 emit_answer 回合后
   发送 finishReason=STOP 的收尾块,但该块带有一个空的 text:"" part,
   被判定为正文而丢弃。缺少 finish_reason 会让 pi 等严格校验的客户端抛出
   "Stream ended without finish_reason" 并按错误自动重试,造成同一条回答
   重复输出多次。现在只把非空正文文本视为产出,收尾块得以正常透传。

本地实测(Antigravity 凭证 / Gemini 3.8 Flash):
- 纯聊天:思考 → 正文 → finish_reason:"stop" + usage → [DONE]
- 工具调用:tool_calls → finish_reason:"tool_calls" + usage → [DONE],
  不再重复下发
- 多轮回路(工具结果回传 → 生成答案)正常收尾

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant