LangChain调用Qwen工具报错?手把手教你魔改ChatOpenAI类,让Xinference完美兼容
LangChain与Xinference工具调用兼容性实战从报错分析到源码级修复当开发者尝试将LangChain与Xinference部署的Qwen模型结合使用时工具调用功能常常成为绊脚石。那些看似简单的示例代码在实际运行中却可能抛出令人困惑的InternalServerError。这不是简单的API调用错误而是两个生态系统在消息格式上的微妙差异所导致的问题。1. 问题现象与初步诊断典型的错误场景是这样的开发者按照官方文档配置好Xinference服务加载了Qwen1.5或Qwen2模型然后兴奋地运行LangChain的工具调用示例代码。然而等待他们的不是预期的计算结果而是这样的报错信息InternalServerError: Error code: 500 - { detail: [address0.0.0.0:30171, pid75345] Tool response dicts require a name key indicating the name of the called function! }这个错误的核心在于消息格式的不匹配。让我们通过一个最小化的复现代码来观察问题from langchain_core.messages import ToolMessage from langchain_openai import ChatOpenAI # 初始化模型 llm ChatOpenAI(base_urlhttp://localhost:9997/v1) # Xinference端点 # 构造工具消息 tool_msg ToolMessage( content16505054784, tool_call_id1 ) # 尝试调用时会触发报错 response llm.invoke([tool_msg])关键问题在于ToolMessage的转换结果缺少了Xinference期望的name字段。通过打印转换后的字典我们可以清楚地看到差异{ content: 16505054784, role: tool, tool_call_id: 1 # 缺少 name 字段 }2. 深入源码分析要真正理解这个问题我们需要深入到两个项目的源码层面。2.1 LangChain的消息处理机制LangChain的ChatOpenAI类在处理消息时会通过_convert_message_to_dict方法将各种消息类型转换为字典格式。对于ToolMessage标准实现只包含三个字段def _convert_message_to_dict(message): if isinstance(message, ToolMessage): return { content: message.content, role: tool, tool_call_id: message.tool_call_id } # 其他消息类型的处理...2.2 Xinference的模板要求Xinference在Qwen1.5/Qwen2的模板中对工具调用响应有严格的格式要求。查看llm_family.json配置文件可以看到类似这样的模板定义{ tool_response_template: { role: tool, name: {{tool_name}}, content: {{tool_content}} } }这种设计选择有其合理性——通过强制包含工具名称可以更好地追踪和调试复杂的工具调用链。但这种设计也造成了与标准OpenAI格式的差异。3. 非侵入式解决方案设计面对这种兼容性问题我们有几种可能的解决路径修改Xinference模板直接调整模型部署配置但这会影响所有使用者预处理消息在调用前手动添加字段但这会增加代码复杂度创建兼容性子类继承并扩展ChatOpenAI实现定制化消息转换第三种方案最为优雅它既能解决问题又不会影响其他代码部分。下面是具体的实现方案from copy import deepcopy from typing import Any, Dict, List, Optional, Union from langchain_core.messages import BaseMessage, ToolMessage from langchain_openai import ChatOpenAI from langchain_openai.chat_models.base import _convert_message_to_dict as original_convert def compatible_convert_message(message: Union[BaseMessage, dict]) - dict: 增强版消息转换函数处理Xinference的特殊要求 if isinstance(message, dict): return deepcopy(message) message_dict original_convert(message) # 特殊处理ToolMessage if isinstance(message, ToolMessage): message_dict[name] message.name or ftool_{message.tool_call_id} return message_dict class XinferenceCompatibleChat(ChatOpenAI): 兼容Xinference工具调用要求的ChatOpenAI子类 def _get_request_payload( self, messages: List[BaseMessage], **kwargs: Any ) - Dict[str, Any]: payload super()._get_request_payload(messages, **kwargs) payload[messages] [compatible_convert_message(m) for m in messages] return payload这个实现有几个关键点保持向后兼容完全保留原有ChatOpenAI的功能只增加必要的修改智能默认值当ToolMessage没有显式设置name时自动生成一个基于tool_call_id的默认值最小侵入不改变原有的消息处理流程只在最后阶段进行适配4. 完整应用示例让我们通过一个完整的数学计算示例展示如何使用这个兼容类from langchain_core.messages import ( HumanMessage, AIMessage, ToolMessage ) from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough # 初始化兼容性客户端 llm XinferenceCompatibleChat( base_urlhttp://localhost:9997/v1, modelqwen1.5-chat ) # 定义few-shot示例 examples [ HumanMessage(317253和128472加4的乘积是多少?, nameuser), AIMessage( , nameassistant, tool_calls[{ name: Multiply, args: {x: 317253, y: 128472}, id: 1 }] ), ToolMessage( 16505054784, tool_call_id1, nameMultiply ), AIMessage( , nameassistant, tool_calls[{ name: Add, args: {x: 16505054784, y: 4}, id: 2 }] ), ToolMessage( 16505054788, tool_call_id2, nameAdd ), AIMessage( 317253和128472加4的乘积是16505054788, nameassistant ) ] # 构建prompt模板 system_prompt 你数学不好但你是使用计算器的专家。以过去使用工具的情况为例说明如何正确使用工具。 prompt ChatPromptTemplate.from_messages([ (system, system_prompt), *examples, (human, {query}) ]) # 创建调用链 chain {query: RunnablePassthrough()} | prompt | llm # 执行查询 result chain.invoke(119乘以8再减20是多少?) print(result.content)这个示例展示了完整的工具调用流程从few-shot学习到实际工具调用所有步骤都能正确处理Xinference的特殊格式要求。5. 进阶讨论与最佳实践在实际项目中应用这种兼容性方案时有几个重要考虑因素5.1 版本兼容性矩阵不同版本的Xinference和Qwen模型对工具调用的支持程度不同模型版本Xinference版本是否需要兼容方案Qwen1.51.15.4是Qwen21.15.4是Qwen2.5≥1.15.4否5.2 性能考量消息转换是每次API调用都会执行的操作因此我们的实现需要注意避免不必要的深拷贝尽量减少条件判断使用高效的字典操作5.3 错误处理增强在原有基础上我们可以增加更健壮的错误处理def compatible_convert_message(message): try: # 原有转换逻辑... except Exception as e: logger.error(f消息转换失败: {str(e)}) raise ValueError(f无法处理消息类型: {type(message)}) from e5.4 单元测试策略为确保兼容性方案的可靠性应该建立完善的测试套件import unittest class TestXinferenceCompatibility(unittest.TestCase): def test_tool_message_conversion(self): msg ToolMessage(test, tool_call_id123) result compatible_convert_message(msg) self.assertIn(name, result) self.assertEqual(result[name], tool_123) def test_missing_name_fallback(self): msg ToolMessage(test, tool_call_id456, nameNone) result compatible_convert_message(msg) self.assertTrue(result[name].startswith(tool_))6. 替代方案比较除了我们采用的子类方案还有其他可能的解决方法各有优缺点6.1 中间件适配器模式class XinferenceAdapter: def __init__(self, llm: ChatOpenAI): self.llm llm def invoke(self, messages): adapted [self._adapt(m) for m in messages] return self.llm.invoke(adapted) def _adapt(self, message): # 适配逻辑...优点更松耦合可以适配不同LLM实现缺点需要额外包装层可能影响调用链组合6.2 Monkey Patchingimport langchain_openai.chat_models.base as openai_base original_convert openai_base._convert_message_to_dict def patched_convert(message): # 修改后的实现... openai_base._convert_message_to_dict patched_convert优点全局生效无需修改调用代码缺点危险可能影响其他不相关代码6.3 自定义Prompt模板通过设计特殊的prompt来规避格式问题请严格按照以下格式响应工具调用 工具名: {{name}} 调用ID: {{tool_call_id}} 结果: {{content}}优点完全不依赖代码修改缺点不可靠增加prompt复杂度相比之下我们的子类方案在灵活性和可靠性之间取得了最佳平衡。