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:
@@ -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` | 服务号在工具图片污染时换会话 |
|
||||
Reference in New Issue
Block a user