【Bug已解决】GRPOTrainer environment_factory / tools is broken for VLMs whose tools return images 解决方案

📅 2026/7/21 23:44:16
【Bug已解决】GRPOTrainer environment_factory / tools is broken for VLMs whose tools return images 解决方案
【Bug已解决】GRPOTrainer environment_factory / tools is broken for VLMs whose tools return images 解决方案一、现象长什么样在用一个视觉语言模型VLM做 agentic RL 时我们给模型配了工具其中一个工具会返回图片比如截图当前页面生成示意图。跑GRPOTrainer的environment_factory/tools流程时训练直接挂或生成出乱码TypeError: can only concatenate str (not PIL.Image.Image) to str或者更隐蔽地图片被str(image)序列化成了PIL.PngImagePlugin.PngImagePlugin object at 0x...模型收到一段无意义文本而非图像多模态上下文被破坏reward 全失。现象特征只在工具返回图片的 VLM 任务上出现纯文本工具返回字符串一切正常报错来自_tool_call_loop工具调用循环里把 tool result 当文本拼回 conversation 的那一步本质是trainer 假设所有 tool result 都是文本没考虑 multimodal 工具结果。二、背景VLM 的对话不是纯文本而是多模态消息一条user/assistant消息的content是一个列表里面混合{type:text,...}和{type:image,...}等块。当模型调用一个返回图片的工具时正确的做法应当是把这张图作为一个新的 image 块插回对话而不是当成字符串拼进 text 块。GRPOTrainer的environment_factory/tools机制设计时是面向纯文本 LLM 的_tool_call_loop大概是这样for call in tool_calls: result execute_tool(call) # 假设 result 是字符串 conversation.append({role: tool, content: str(result)}) # ← 文本假设当result是PIL.Image.Image时str(result)得到对象地址模型看到的不是图而是垃圾文本或者str content直接TypeError。VLM 的多模态上下文因此被破坏——这正是 issue 描述的broken for VLMs whose tools return images。三、根因根因一句话GRPOTrainer 的_tool_call_loop把 tool result 一律当作文本处理字符串拼接 / 文本消息没有识别并正确插入多模态图像结果导致 VLM 的工具返回图片时上下文损坏或报错。具体文本假设content被当字符串图片被str()或拼接丢失图像语义消息结构不匹配VLM 需要content是[{type:image}, ...]的块列表但 trainer 塞了个文本串environment_factory 没有 content-type 协商工厂返回 tool result 时没标注这是图trainer 无从得知该插 image 块还是 text 块静默损坏有时不报错只是str(image)变垃圾文本模型拿到错误上下文reward 崩。本质是工具结果的多模态类型在 trainer 里没有一等公民地位。四、最小可运行复现下面用纯 Python不依赖 PIL用占位对象复现图片被当文本拼的崩溃与损坏class FakeImage: def __str__(self): return FakeImage object def tool_call_loop_text_only(conversation, tool_results): 原实现假设所有 tool result 是文本。 for r in tool_results: conversation.append({role: tool, content: str(r)}) # 图片被 str return conversation def demo(): conv [{role: user, content: 截图给我看}] img FakeImage() try: out tool_call_loop_text_only(conv, [img]) print(没报错但 content , repr(out[-1][content])) # 垃圾文本 except TypeError as e: print(直接 TypeError, e) if __name__ __main__: demo()输出没报错但 content FakeImage object这正是静默损坏图片没被当图处理而是变成无意义字符串塞进对话VLM 拿到的多模态上下文是坏的。某些框架里这一步是str content拼接则会直接TypeError挂掉。五、解决方案第一层让 tool result 携带类型插入正确块第一层给 tool result 一个明确的类型标记循环里按类型插入 image 块或 text 块from typing import Dict, List, Any, Union def make_tool_result(payload: Any, kind: str text) - Dict[str, Any]: 统一工具结果契约显式声明类型。 return {kind: kind, payload: payload} def append_tool_result(conversation: List[Dict], result: Dict[str, Any]) - List[Dict]: 按类型把工具结果插回对话VLM 友好。 conversation list(conversation) if result[kind] image: # VLMcontent 是块列表插入 image 块 block {type: image, image: result[payload]} conversation.append({role: tool, content: [block]}) else: conversation.append({role: tool, content: str(result[payload])}) return conversation def demo(): conv [{role: user, content: 截图给我看}] img_result make_tool_result(PNG bytes, kindimage) out append_tool_result(conv, img_result) print(VLM 对话块, out[-1]) if __name__ __main__: demo()核心改动make_tool_result显式标kindappend_tool_result对image类型插入{type:image,image:...}块对文本插入字符串。多模态上下文不再损坏。六、解决方案第二层environment_factory 返回结构化结果trainer 统一分派第二层让environment_factory返回的就是带类型的结果trainer 不再猜测from typing import Dict, List, Any def environment_factory(task): 工厂按任务返回带类型的工具结果示意。 def run_tool(tool_name, args): if tool_name screenshot: image fimg:{args} # 真实场景是 PIL.Image return make_tool_result(image, kindimage) return make_tool_result(fresult for {args}, kindtext) return run_tool def tool_call_loop_vlm(conversation, calls, run_tool): for call in calls: result run_tool(call[name], call[args]) conversation append_tool_result(conversation, result) return conversation def demo(): factory environment_factory(web_task) conv [{role: user, content: 截个图}] calls [{name: screenshot, args: home}] out tool_call_loop_vlm(conv, calls, factory) print(工厂返回结构化结果trainer 正确分派, out[-1]) if __name__ __main__: demo()environment_factory返回的结果自带kind_tool_call_loop直接按类型分派彻底消除trainer 猜类型是文本的假设。VDM/VLM 与文本 LLM 走同一套循环只是 image 类型走不同插入分支。七、解决方案第三层处理器对齐 不变量测试第三层保证插入的 image 块能被 processortokenizerimage_processor正确编码并加测试锁住图片结果不变成文本from typing import Dict, List, Any def to_processor_content(message) - Any: 把 tool 消息转成 processor 能吃的格式VLM 是块列表文本是字符串。 content message[content] if isinstance(content, list): # 已是块列表含 imageprocessor 会处理 return content return content def assert_no_image_as_text(conversation: List[Dict]): for m in conversation: if m.get(role) tool: c m[content] if isinstance(c, str) and object at 0x in c: raise AssertionError(图片被错误序列化成文本多模态上下文损坏) def test_image_inserted_as_block(): conv [{role: user, content: 截图}] res make_tool_result(PNG, kindimage) conv append_tool_result(conv, res) assert_no_image_as_text(conv) assert isinstance(conv[-1][content], list), 图片应作为块列表插入 assert conv[-1][content][0][type] image print(OK: 图片作为 image 块插入未被文本化) if __name__ __main__: test_image_inserted_as_block()assert_no_image_as_text在训练主循环每步调用一旦图片被str()成对象地址就立刻断言失败把静默损坏变成显式报错test_image_inserted_as_block锁住图片结果为块列表、typeimage的不变量。八、接入 GRPOTrainer 的建议如果你要在GRPOTrainer里支持工具返回图片的 VLM建议改 tool result 契约工具/工厂返回{kind, payload}显式标注 image/text。改_tool_call_loop按kind分派image 插{type:image,image:...}块。工厂结构化environment_factory返回带类型结果trainer 不猜测。processor 对齐确认 image 块能被processor.__call__正确编码配processor_kwargs。加护栏断言assert_no_image_as_text每步检查防回归。加不变量测试锁住图片不文本化、作为块插入。九、排查清单如果你在VLM 工具返回图片时遇到挂掉/乱码按顺序查看报错是否can only concatenate str或str(image)变垃圾是则图片被当文本。搜_tool_call_looptool result 是否被str()或拼进文本 content。确认对话 content 结构VLM 的 tool 消息应为块列表而非字符串。改 result 契约为{kind,payload}显式标 image/text。改插入逻辑image 类型插{type:image}块。确认 processor 能吃image 块能否被 processor 编码。加断言/测试锁住图片不文本化。十、小结GRPOTrainer的environment_factory/tools在 VLM 工具返回图片时崩溃或乱码根因是工具调用循环把所有 tool result 一律当文本处理字符串拼接 / 文本消息没有识别并正确插入多模态图像块。图片被str()成对象地址或触发TypeErrorVLM 的多模态上下文被破坏reward 全失。它有时会静默损坏不报错只变垃圾文本更难察觉。修复分三层第一层给 tool result 加kind类型标记append_tool_result按类型把图片插成{type:image}块、文本插成字符串从契约上消除损坏第二层让environment_factory返回结构化结果trainer 直接分派不再猜测类型第三层加assert_no_image_as_text护栏与图片作为块插入不变量测试把静默损坏变成显式报错。核心心法是当工具结果可能是多模态时其类型必须是一等公民——trainer 不能假设结果永远是文本而应按类型把图像正确插回多模态对话结构。