fix(vision): stop read_image from poisoning text-provider sessions

Image turns already get a vision-model description injected into the prompt,
but the agent kept calling read_image to "confirm" the pictures. Those tool
results carry base64 image parts that Goose persists, so every later turn
against the text-only chat provider failed with `unknown variant image_url`
before the agent could write the page. WeChat page requests therefore fell
through to the fail-closed delivery message.

Drop read_image for the turn whenever a vision model handles the images, say
so explicitly in the injected prompt, and teach the poison scan to recognise
tool image parts so already-polluted sessions rotate instead of failing again.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-22 10:40:12 +08:00
parent 4d12ea438b
commit ef4ce12bbf
9 changed files with 457 additions and 6 deletions
+1
View File
@@ -15,6 +15,7 @@
| [memory-v2-candidate-and-lifecycle.md](./memory-v2-candidate-and-lifecycle.md) | 候选记忆表幂等初始化、Portal fail-open、生命周期 off/canary/active 作用域 |
| [episodic-history-recall.md](./episodic-history-recall.md) | 历史会话召回、用户隔离、旧快照回退、提示注入与 off/canary/active 灰度 |
| [mindspace-seo-geo.md](./mindspace-seo-geo.md) | 公开页 SEO/GEO 注入、私有页 noindex、sitemap/llms.txt 与百度推送开关 |
| [vision-turn-read-image-isolation.md](./vision-turn-read-image-isolation.md) | 图片轮次禁用 `read_image`、视觉提示硬约束、`read_image` 工具图片污染检测与会话轮换 |
## 自动化
@@ -0,0 +1,69 @@
# 图片轮次读图工具隔离守卫
## 已知故障
2026-08-22 服务号用户唐连发 4 张图后要求「把那几张图片做成主题页面」,连续三次没有拿到页面。
链路是这样断的:
1. `attachRecentMediaForFollowup` 正确把 4 张图挂到了这一轮,Qwen VL 也成功产出了图片描述,
并注入进 Agent 提示。
2. Agent 拿到描述后,仍然逐张调用 `read_image` 去「确认图片内容」。
3. `read_image` 的工具结果不是 `image_url`,而是嵌在 `toolResponse.toolResult.value.content[]`
里的 base64 `image` 块,4 张图累计 1.5MB 以上。Goose 会把它们持久化进会话历史。
4. 下一次请求把整段历史发给纯文本聊天模型(DeepSeek),上游直接返回
`unknown variant image_url, expected text`Agent 还没走到 `write_file` 就中断。
5. 页面交付是 fail-closed 的:没有新落盘的 HTML,就不会发占位链接,用户只看到
「这次页面没有按服务号页面技能真正生成成功」。
原有的两道防线都没拦住:
- `conversationHasImageUrlContent` 只扫 `image_url`,看不见 `read_image` 留下的工具图片块,
所以微信侧的会话轮换不会触发,同一个被污染的会话被反复复用。
- `agent-run-gateway``SESSION_VISUAL_CONTEXT_UNSUPPORTED` 视觉降级是**事后**补救,
只覆盖 H5 Agent Run,服务号回复路径没有对应保护。
## 必须保留的行为
1. 本轮消息带图且已配置图片模型(`llmProviderService.hasVisionKey()`)时,
`prepareSessionReplyBody` 必须以 `disableImageReading` 下发会话策略,
使这一轮的 `developer` 扩展不含 `read_image`。视觉理解由图片模型独占,
聊天模型不承担读图。
2. 不带图的轮次必须保留 `read_image`。禁用是按轮次生效的,不能把会话永久降级。
3. `buildVisionPayload` 注入的提示必须明确写出「本轮不会再提供读图工具 + 禁止调用
`read_image`」,并说明强行读图会让本轮及后续请求全部失败。
4. 图片模型没能产出描述时,提示必须改口为「不要臆造画面细节」,不能继续声称
「依据上面的描述写文案」。此时仍然禁用读图 —— 纯文本模型看不到图,
放开 `read_image` 只会让整轮崩掉,而图片嵌入路径依然可用。
5. `conversationHasToolImageContent` 必须能识别 `toolResponse` / `tool_response`
`toolResult.value.content[].type === 'image'` 的工具图片块,
并覆盖 `toolResult` / `tool_result` / 直接数组等结构变体。
6. `rotateWechatSessionIfImagePolluted` 必须同时检查 `image_url` 污染和工具图片污染,
任一命中都要在回复前换掉会话。历史遗留的被污染会话靠这条自愈。
7. `agent-run-gateway` 的事后视觉降级(`SESSION_VISUAL_CONTEXT_UNSUPPORTED`
新会话 + `disableImageReading: true`)保留为兜底,不得因为新增事前预防而删除。
## 改动前必跑
```bash
node --test tkmind-proxy.test.mjs chat-image-turn-scope.test.mjs wechat-mp.test.mjs agent-run-gateway.test.mjs
npm run verify:h5-session-patches
```
## 相关代码与用例
| 位置 | 作用 |
|------|------|
| `tkmind-proxy.mjs` · `prepareSessionReplyBody` | 事前禁用本轮 `read_image` |
| `tkmind-proxy.mjs` · `buildVisionPayload` | 注入禁止读图的硬性提示 |
| `chat-image-turn-scope.mjs` · `conversationHasToolImageContent` | 识别 `read_image` 工具图片污染 |
| `wechat-mp.mjs` · `rotateWechatSessionIfImagePolluted` | 回复前轮换被污染会话 |
| `capabilities.mjs` · `withoutSessionImageRead` | 从会话策略里摘掉 `read_image` |
| 用例 | 覆盖 |
|------|------|
| `tkmind-proxy.test.mjs` · `image turns drop read_image so vision results cannot poison the text provider` | 带图轮次下发的 `developer` 工具不含 `read_image` |
| `tkmind-proxy.test.mjs` · `text-only turns keep read_image available` | 纯文本轮次不降级 |
| `tkmind-proxy.test.mjs` · `submitSessionReplyForUser applies the shared Qwen vision preprocessing path` | 提示中包含禁止读图约束 |
| `chat-image-turn-scope.test.mjs` · `conversationHasToolImageContent detects read_image base64 poison` | 工具图片块识别,且 `image_url` 扫描确实看不见它 |
| `wechat-mp.test.mjs` · `wechat mp rotates a session poisoned by read_image tool results` | 服务号在工具图片污染时换会话 |