大模型工具调用的另类用法——结构化json输出
一. 原理
在我的上一篇笔记中,记录了如何使用原生的大模型进行工具调用:https://www.cnblogs.com/nanimono/p/19295032。让大模型进行工具调用本质上并不是让模型自动调用工具并返回结果,而是:
1. 在输入给模型的数据中定义对工具(函数)名字、功能、参数名及介绍的标准化描述,目前推荐使用JSON Schema来进行标准化描述。
JSON Schema例子:
{ "type": "function", "function": { "name": "get_weather", "description": "Get weather of a location, the user should supply the location and date.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city name" }, "date": { "type": "string", "description": "The date in format YYYY-mm-dd" }, }, "required": ["location", "date"] }, } }
类似这样的格式就叫做JSON Schema。如果模型原生支持工具调用,这部分会放在tools参数中;如果不支持,就放在prompts里面,并用文字提示强调模型进行必要的工具调用。
2. 模型接到工具调用的说明schema及指令后,如果需要调用工具,会生成一个工具消息来描述自己准备调用的工具,如下面的例子:
ChatCompletionMessageFunctionToolCall( id='call_00_eDQNUXjobdtx9DJI9ZiVsbhX', function=Function( arguments='{"location": "北京", "date": "2025-12-01"}', name='get_weather' ), type='function', index=0 )
3. 用户接收到工具消息后,解析出里面的function.name和function.arguments(arguments是一个JSON字符串),手动进行调用,得到的结果再组装成ToolMessage,跟历史消息一起回传给大模型,让大模型给出最终答案。
============== 以上是工具调用的正常用法 ===============
那么什么是工具调用的非正常用法?利用工具调用的特性,让大模型进行结构化JSON输出。
一般情况下,大模型的输出是一些比较自由的文本,就像跟用户在聊天一样。但在开发场景下,大多数时候,大模型的输出我们还要交给其它程序去进一步处理,所以更希望它输出一些标准的结构化数据,最好的选择就是JSON。
但无论我们怎么在prompt里强调,模型总是有概率跑偏,如何让模型能够比较稳定地进行结构化输出呢?
方案一:对于支持结构化输出的大模型,可以在原生调用模型api时,传入参数'response_format': {'type': 'json_object'},并且在prompt中必须含有 json 字样,并给出希望模型输出的 JSON 格式的样例:
1 import json 2 from openai import OpenAI 3 4 client = OpenAI( 5 api_key="<your api key>", 6 base_url="https://api.deepseek.com", 7 ) 8 9 system_prompt = """ 10 The user will provide some exam text. Please parse the "question" and "answer" and output them in JSON format. 11 12 EXAMPLE INPUT: 13 Which is the highest mountain in the world? Mount Everest. 14 15 EXAMPLE JSON OUTPUT: 16 { 17 "question": "Which is the highest mountain in the world?", 18 "answer": "Mount Everest" 19 } 20 """ 21 22 user_prompt = "Which is the longest river in the world? The Nile River." 23 24 messages = [{"role": "system", "content": system_prompt}, 25 {"role": "user", "content": user_prompt}] 26 27 response = client.chat.completions.create( 28 model="deepseek-chat", 29 messages=messages, 30 response_format={ 31 'type': 'json_object' 32 } 33 ) 34 35 print(json.loads(response.choices[0].message.content))
方案二:对于不支持结构化输出的模型,或者结构化输出表现不太好的模型。观察前面模型调用工具的例子,我们会发现,模型在准备调用工具时,输出的工具调用消息,里面的arguments是一个标准JSON字符串!这说明大模型在认为自己要调用工具时,会相对比较稳定地输出JSON。
那我们可以利用大模型的这个特性,定义一种“特殊”的工具,强制让大模型调用这个工具,它输出的工具调用参数就是我们想要的结构化输出了。
那么这个“特殊”工具怎么定义呢?参考上面我们可以知道,只需要定义一个标准JSON Schema:
{ "type": "function", "function": { # 函数名和描述可以随意定义 "name": "format_output", "description": "这是一个结构化输出方法,输出前必须调用", "parameters": { "type": "object", "properties": { # 需要结构化输出的数据描述放在这里 "location": { "type": "string", "description": "The city name" }, "date": { "type": "string", "description": "The date in format YYYY-mm-dd" }, }, # 必填项,根据需要的输出情况填写 "required": ["location", "date"] }, } } }
然后在调用模型api时,用上面的schema填充tools(注意tools是个数组),指定tool_choice参数为'required'(不同模型或框架值略有不同,要参照对应文档),强制模型调用结构化输出方法,即可。
二. 封装
langchain有一个llm.with_struct_output()方法,帮助开发者完成结构化输出,这个方法支持两种模式:'function_calling'和'json_mode',分别对应上面的两种方案。
with_struct_output()核心部分源码:
1 def with_structured_output( 2 self, 3 schema: Optional[_DictOrPydanticClass] = None, 4 *, 5 method: Literal["function_calling", "json_mode"] = "function_calling", 6 include_raw: bool = False, 7 **kwargs: Any, 8 ) -> Runnable[LanguageModelInput, _DictOrPydantic]: 9 if kwargs: 10 raise ValueError(f"Received unsupported arguments {kwargs}") 11 is_pydantic_schema = _is_pydantic_class(schema) 12 if method == "function_calling": 13 if schema is None: 14 raise ValueError( 15 "schema must be specified when method is 'function_calling'. " 16 "Received None." 17 ) 18 llm = self.bind_tools([schema], tool_choice="any") 19 if is_pydantic_schema: 20 output_parser: OutputParserLike = PydanticToolsParser( 21 tools=[schema], first_tool_only=True 22 ) 23 else: 24 key_name = convert_to_openai_tool(schema)["function"]["name"] 25 output_parser = JsonOutputKeyToolsParser( 26 key_name=key_name, first_tool_only=True 27 ) 28 elif method == "json_mode": 29 llm = self.bind(response_format={"type": "json_object"}) 30 output_parser = ( 31 PydanticOutputParser(pydantic_object=schema) 32 if is_pydantic_schema 33 else JsonOutputParser() 34 ) 35 else: 36 raise ValueError( 37 f"Unrecognized method argument. Expected one of 'function_calling' or " 38 f"'json_mode'. Received: '{method}'" 39 ) 40 41 if include_raw: 42 parser_assign = RunnablePassthrough.assign( 43 parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None 44 ) 45 parser_none = RunnablePassthrough.assign(parsed=lambda _: None) 46 parser_with_fallback = parser_assign.with_fallbacks( 47 [parser_none], exception_key="parsing_error" 48 ) 49 return RunnableMap(raw=llm) | parser_with_fallback 50 else: 51 return llm | output_parser
这个方法的参数有三个:schema、method、和include_raw。schema可以传标准JSON Schema或者Pydantic类;method就是指定两种模式,默认function_calling;include_raw默认False,表示是否包含原始输出(暂时不关心这个)。
插播一条关于Pydantic类的简要介绍,前面的JSON Schema虽然准确,但代码比较长。Pydantic类似前端的TypeScript,可以用类的方式,用更少的代码描述JSON Schema。
from langchain_core.pydantic_v1 import BaseModel, Field class QAExtra(BaseModel): """一个问答键值对工具,传递对应的假设性问题+答案""" question: str = Field(description="假设性问题") answer: str = Field(description="假设性问题对应的答案") # 在Pydantic类的处理中,会调用如下语句,将pydantic转化成标准JSON Schema QAExtra.model_json_schema()
完整的从Pydantic到伪装工具调用流程:
Pydantic类 Weather ↓ model_json_schema() JSON Schema ↓ convert_to_openai_tool() OpenAI Tool (name="Weather") ↓ bind_tools() LLM被"强制"调用Weather工具 ↓ 输出 tool_calls PydanticToolsParser 解析为 Weather 实例
如果method选择了json_object模式,就比较简单了,直接设置llm.bind(response_format={"type": "json_object"}),但注意要在prompt中插入schema或者JSON的描述。在json_object模式中,没有用到传入的schema。
源码中在进行工具调用后,会使用特定的OutputParser将模型返回转成python dict,方便后续使用。
三. 总结
以上就是让智能体进行结构化输出的两种方式,无论是框架还是自己构造方法,其基本原理都是一样的。
同时,比较推荐使用Pydantic类对输出进行结构化描述和校验,就算你使用response_format为json_object的形式,也可以将Pydantic类使用model_json_schema()转化成schema后,使用模板插值替换到prompt中,来规范化模型prompt,防止手动定义出错。
如果使用langchain的with_struct_output()方法,则应该明白这个方法的底层原理,不要只是单纯使用。
浙公网安备 33010602011771号