LangChain 学习笔记(二):Tool Calling、ToolMessage 与结构化输出到底怎么串起来

发布时间:2026/7/9 9:39:56

LangChain 学习笔记(二):Tool Calling、ToolMessage 与结构化输出到底怎么串起来 LangChain 学习笔记二Tool Calling、ToolMessage 与结构化输出到底怎么串起来这是 LangChain 学习笔记系列的第二篇。第一篇主要整理了ChatModel、Message、PromptTemplate和 LCEL 调用链路。这一篇继续往下走重点讲 LangChain 里非常核心的两件事工具调用和结构化输出。1. 为什么需要 Tool Calling大模型本身最擅长的是理解自然语言 生成自然语言 总结、改写、推理、翻译但它本身不会真的做这些事情查数据库 搜索网页 读取本地文件 调用天气接口 发送邮件 执行计算函数 访问业务系统比如用户问北京今天的天气怎么样模型如果只靠训练知识回答就很容易过时。再比如用户问帮我查一下订单 10086 的物流状态。模型自己并不知道你的订单系统在哪里也不能直接查数据库。所以 Tool Calling 要解决的问题是让模型负责判断“该调用什么工具、参数是什么” 让程序负责真正执行工具 再把工具结果交回模型生成最终回答。一句话模型负责决策和表达代码负责执行真实动作。2. Tool Calling 的核心流程先看整体流程用户提问 - 模型判断是否需要工具 - 模型返回 AIMessage里面包含 tool_calls - Python 程序读取 tool_calls - 执行对应工具 - 工具结果包装成 ToolMessage - 把 ToolMessage 放回 messages - 再次调用模型 - 模型生成最终自然语言回答注意一个关键点AI 不直接执行工具。 AI 只生成 tool_calls。 真正执行工具的是 Python 程序。这也是很多初学者最容易混的地方。3. 定义工具tool最简单的工具定义方式是tool。fromlangchain_core.toolsimporttooltooldefmultiply(a:int,b:int)-int:Multiply two numbers.returna*b这个函数看起来只是普通 Python 函数但加上tool后LangChain 会把它包装成模型可理解的工具。工具函数最好包含函数名 参数类型注解 docstring 描述 返回值因为模型会根据这些信息判断这个工具是干什么的 什么时候应该调用 调用时需要传什么参数例如tooldefadd(a:int,b:int)-int:Add two integers.returnab这里的add、a、b、Add two integers.都会成为工具 schema 的一部分。4. 定义工具StructuredTool如果工具参数比较复杂或者想显式定义参数 schema可以用StructuredTool。frompydanticimportBaseModel,Fieldfromlangchain_core.toolsimportStructuredToolclassCalculatorInput(BaseModel):a:intField(description第一个数字)b:intField(description第二个数字)defmultiply(a:int,b:int)-int:returna*b calculator_toolStructuredTool.from_function(funcmultiply,nameCalculator,description两数相乘,args_schemaCalculatorInput)StructuredTool的优势是参数结构更清晰 字段描述更明确 更适合复杂工具 更方便模型理解和校验参数。简单工具用tool就够了。复杂工具、业务接口、参数较多的函数更适合StructuredTool。5. 绑定工具bind_tools工具定义好后需要绑定到模型tools[add,multiply]model_with_toolsmodel.bind_tools(tools)这一步很重要。它不是执行工具而是告诉模型你现在可以使用 add 和 multiply 这两个工具。所以model_with_toolsmodel.bind_tools(tools)返回的是一个新的 Runnable。后面调用ai_msgmodel_with_tools.invoke(2乘3等于多少)模型可能不会直接回答而是返回我要调用 multiply 工具参数是 a2, b3。6.tool_calls是什么tool_calls是模型返回的工具调用请求列表。例如用户问2乘3等于多少6加6等于多少模型可能返回[{name:multiply,args:{a:2,b:3},id:call_abc123,type:tool_call},{name:add,args:{a:6,b:6},id:call_def456,type:tool_call}]字段含义name要调用哪个工具 args传给工具的参数 id这次工具调用的唯一编号 type类型表示这是工具调用重点tool_calls 不是工具执行结果。 tool_calls 是模型生成的“工具调用计划”。真正执行工具的是后面的 Python 代码。7. 完整工具调用代码下面是一段完整流程fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportHumanMessagefromlangchain_core.toolsimporttool modelChatOpenAI(modelgpt-4o-mini)tooldefadd(a:int,b:int)-int:Add two integers.returnabtooldefmultiply(a:int,b:int)-int:Multiply two integers.returna*b tools[add,multiply]model_with_toolsmodel.bind_tools(tools)messages[HumanMessage(2乘3等于多少6加6等于多少)]ai_msgmodel_with_tools.invoke(messages)messages.append(ai_msg)fortool_callinai_msg.tool_calls:selected_tool{add:add,multiply:multiply,}[tool_call[name].lower()]tool_msgselected_tool.invoke(tool_call)messages.append(tool_msg)final_msgmodel.invoke(messages)print(final_msg.content)这段代码可以拆成四步第一步用户提问 第二步模型生成 tool_calls 第三步程序执行工具得到 ToolMessage 第四步模型根据工具结果生成最终答案最终模型大概率会回答2乘3等于66加6等于12。8. 为什么要把ToolMessage放回 messages这是工具调用里最关键的一点。执行工具后结果只存在于 Python 程序里tool_msgselected_tool.invoke(tool_call)模型并不知道这个结果。如果不把工具结果放回消息列表模型只知道我刚才想调用 multiply 和 add。但它不知道multiply 返回了 6 add 返回了 12。所以必须messages.append(tool_msg)让模型在第二次调用时看到完整上下文HumanMessage(2乘3等于多少6加6等于多少) AIMessage(tool_calls[...]) ToolMessage(6, tool_call_idcall_abc123) ToolMessage(12, tool_call_idcall_def456)这就是工具调用的标准协议HumanMessage - AIMessage(tool_calls) - ToolMessage - AIMessage(final answer)tool_call_id的作用ToolMessage里会带一个tool_call_id。它的作用是告诉模型这个工具结果对应前面哪一次工具调用。如果一次模型请求里产生多个工具调用比如同时调用multiply和add模型需要知道哪个结果是 multiply 的 哪个结果是 add 的。这就是tool_call_id的价值。9. TavilySearch搜索工具的例子工具不一定是数学函数也可以是搜索工具。比如使用 TavilySearchfromlangchain_tavilyimportTavilySearchfromlangchain_core.messagesimportHumanMessage toolTavilySearch(max_results4)model_with_toolsmodel.bind_tools([tool])messages[HumanMessage(北京今天的天气怎么样)]ai_msgmodel_with_tools.invoke(messages)messages.append(ai_msg)fortool_callinai_msg.tool_calls:tool_msgtool.invoke(tool_call)messages.append(tool_msg)final_msgmodel.invoke(messages)print(final_msg.content)这里第一次调用模型时模型可能生成我要调用 TavilySearch搜索“北京今天的天气”。程序执行搜索后得到搜索结果。然后把搜索结果作为ToolMessage放回消息列表。最后模型再根据搜索结果总结成自然语言。这个流程非常接近真实 AI 应用模型判断要查什么 - 工具查真实数据 - 模型基于真实数据回答10.bind_tools里几个常见参数bind_tools还有一些控制参数。10.1tool_choice控制模型是否必须使用工具。常见值auto 自动选择模型可以用也可以不用 none 不调用工具 any 强制至少调用一个工具 required 强制调用工具例如model_with_toolsmodel.bind_tools(tools,tool_choiceany)表示强制模型至少调用一个工具。10.2parallel_tool_calls控制是否允许并行工具调用。如果用户一次问了两个问题2乘3是多少6加6是多少模型可能一次性生成两个工具调用multiply add这就是并行工具调用的场景。10.3strictstrictTrue时会更严格要求模型输出符合工具 schema。它适合对参数格式要求严格的业务场景。11. 为什么还需要结构化输出工具调用解决的是模型如何调用外部能力。结构化输出解决的是另一个问题模型输出如何变成程序能稳定使用的数据。普通模型输出可能是这个笑话的开头是……妙语是……我给 8 分。对人来说能看懂但程序很难稳定处理。我们更希望得到{setup:为什么程序员喜欢黑夜,punchline:因为黑夜里 bug 比较少。,rating:8}这样前端可以展示后端可以入库业务逻辑可以继续处理。12. 用 Pydantic 定义输出结构先定义一个数据模型fromtypingimportOptionalfrompydanticimportBaseModel,FieldclassJoke(BaseModel):给用户讲的一个笑话setup:strField(description这个笑话的开头)punchline:strField(description这个笑话的妙语)rating:Optional[int]Field(defaultNone,description从1-10分给这个笑话评分)这里的Joke表示模型最终应该输出一个笑话对象 对象里有 setup、punchline、rating 三个字段。字段含义setup笑话开头必须是字符串 punchline笑话妙语必须是字符串 rating评分可以是整数也可以是 None注意Joke 只是结构定义。 Joke 本身不会调用模型也不会自动约束模型输出。真正让模型按这个结构输出需要 LangChain 的结构化输出能力或输出解析器。13.with_structured_output第一种方式是structured_modelmodel.with_structured_output(Joke)然后调用resultstructured_model.invoke(讲一个关于程序员的笑话)print(result)返回结果可能直接是Joke(setup为什么程序员喜欢黑夜,punchline因为黑夜里 bug 比较少。,rating8)这种方式可以理解成把 Joke 这个 schema 绑定到模型上 让模型按照结构化格式返回 LangChain 再把结果解析成 Joke 对象。它通常比纯 prompt 指令更稳尤其是模型服务商支持 tool calling 或 JSON schema 的时候。14.PydanticOutputParser第二种方式是输出解析器fromlangchain_core.output_parsersimportPydanticOutputParser parserPydanticOutputParser(pydantic_objectJoke)它拿着Joke做两件事。第一生成格式说明parser.get_format_instructions()这会生成一段提示词大意是请按照下面的 JSON schema 输出 setup 是字符串 punchline 是字符串 rating 是整数或 null第二解析模型输出chainprompt|model|parser它会把模型返回的 JSON 字符串解析成Joke对象。15. 为什么 prompt 里用了 parser链尾还要放 parser这是一个很容易混淆的问题。示例代码promptPromptTemplate(template回答用户问题。\n返回结构说明{format_instructions}\n用户问题{query}\n,partial_variables{format_instructions:parser.get_format_instructions()},input_variables[query],)chainprompt|model|parser看起来好像 parser 用了两次。但这两次作用完全不同。第一次放进 prompt 里parser.get_format_instructions()这里不是把 parser 本体放进去而是生成一段格式说明文本。作用是告诉模型应该按什么格式输出。它发生在模型调用之前。第二次放在 model 后面chainprompt|model|parser这里的 parser 是真正解析模型输出。作用是把模型返回的 JSON 字符串解析成 Joke 对象。它发生在模型调用之后。类比一下prompt 里的 format_instructions考前答题要求 链尾的 parser考后批改和整理答案如果只放格式说明不放链尾 parserchainprompt|model你拿到的可能只是AIMessage(content{setup: ..., punchline: ..., rating: 8})它看起来像 JSON但本质还是字符串。如果只放链尾 parser不给格式说明chainprompt|model|parser但 prompt 里没告诉模型输出 JSON模型可能输出一段自然语言parser 就可能解析失败。所以两步都需要format_instructions提高模型按格式输出的概率 parser把输出真正转成 Python 对象16.JsonOutputParserJsonOutputParser用来解析 JSON 输出。它可以不绑定 Pydantic也可以配合 Pydantic 做验证。例如fromlangchain_core.output_parsersimportJsonOutputParser parserJsonOutputParser()适合场景只需要普通 JSON 字典 字段结构不算特别复杂 前后端接口需要 JSON如果配合 PydanticparserJsonOutputParser(pydantic_objectJoke)它也可以根据Joke做一定的结构约束。17. Pydantic 和 JSON 的区别简单理解JSON 是数据格式。 Pydantic 是数据模型 类型校验。如果只要{name:小明,age:18}用 JSON 就可以。但如果你希望更严格name 必须是字符串 age 必须是整数 rating 可以为空 字段缺失时怎么处理 字段说明是什么那就更适合 Pydantic。实际项目里简单结构JsonOutputParser 强类型结构PydanticOutputParser 或 with_structured_output18.with_structured_output和PydanticOutputParser怎么选两种方式都能得到结构化结果但机制不同。with_structured_output把 schema 绑定到模型上 通常使用模型服务商支持的 tool calling / JSON schema 等能力 更像模型原生结构化输出 一般更稳。适合支持结构化输出的聊天模型 希望直接得到 Pydantic 对象 希望减少手动 prompt 控制。PydanticOutputParser通过格式说明告诉模型怎么输出 再用 parser 解析模型返回内容 更偏 prompt 约束 后处理解析。适合想手动控制 prompt 需要学习输出解析器原理 模型不一定支持原生结构化输出 想和 PromptTemplate 显式组合。一句话with_structured_output 更像“模型绑定 schema” PydanticOutputParser 更像“提示模型按 schema 写然后我来解析”。19. 工具调用和结构化输出的关系这两个概念容易混在一起。它们都可能使用 schema但解决的问题不同。Tool Calling 解决“模型如何调用外部函数/工具”的问题。 结构化输出 解决“模型最终回答如何变成结构化数据”的问题。例如用户问北京今天适合跑步吗可能流程是1. 模型调用 TavilySearch 查询天气 2. 程序执行搜索工具 3. 工具结果返回模型 4. 模型输出结构化建议 { suitable: true, reason: ..., risk_level: low }这里同时用了Tool Calling查天气 结构化输出返回固定格式建议20. 初学者容易混淆的点20.1tool_calls是不是工具结果不是。tool_calls 是模型想调用工具的请求。 ToolMessage 才是工具执行结果。20.2 模型会不会自己执行工具不会。模型只生成调用计划 Python 程序真正执行工具。20.3 为什么要把 AIMessage 也放回 messages因为ToolMessage必须对应前面的AIMessage(tool_calls)。模型需要看到我刚才请求了哪些工具 每个工具返回了什么。20.4Joke和PydanticOutputParser功能是不是重复不重复。Joke定义结构。 Parser使用这个结构生成格式说明并解析模型输出。20.5 prompt 里已经有格式说明为什么链尾还要 parser因为格式说明只是告诉模型怎么写。链尾 parser 才是真正把模型输出转成 Python 对象。21. 真实项目里怎么用这一篇的内容在真实项目里非常有用。21.1 工具调用能做什么AI 查询数据库 AI 搜索网页 AI 调用天气接口 AI 读取本地文件 AI 调用订单系统 AI 执行计算函数 AI 操作业务 API它是 Agent 的基础能力。21.2 结构化输出能做什么简历分析结果入库 客服意图分类 合同字段抽取 论文信息抽取 商品评论情感分析 RAG 回答结构化总结比如做一个简历分析助手可以让模型输出{name:张三,skills:[Python,LangChain,Redis],match_score:82,suggestions:[补充项目经历,加强 RAG 实践]}这比返回一大段自然语言更适合程序处理。22. 本篇总结这一篇主要讲了两件事。第一工具调用模型生成 tool_calls Python 执行工具 工具结果变成 ToolMessage 再交回模型生成最终回答。标准流程是HumanMessage - AIMessage(tool_calls) - ToolMessage - AIMessage(final answer)第二结构化输出用 Pydantic 或 JSON schema 规定模型输出结构 用 with_structured_output 或 OutputParser 得到程序能直接处理的数据。几个关键结论tool_calls 是工具调用请求不是结果。 ToolMessage 是工具执行结果。 tool_call_id 用来对应一次具体工具调用。 Joke 这类 Pydantic 类只定义结构不负责调用模型。 PydanticOutputParser 负责生成格式说明和解析模型输出。 with_structured_output 更像把结构直接绑定给模型。一句话概括Tool Calling 让模型能连接外部能力 结构化输出让模型结果能被程序稳定使用。23. 下一篇预告下一篇会整理Few-shot、示例选择器与 RAG 检索重点回答这些问题Few-shot 为什么能让模型模仿格式和推理方式 FewShotPromptTemplate 和 FewShotChatMessagePromptTemplate 有什么区别 Length、语义相似、MMR、N-gram 示例选择器分别怎么选示例 RAG 为什么要先检索再生成 Retriever 和 VectorStore 是什么关系 MMR 在 RAG 里为什么能减少重复文档也就是从“模型如何调用工具和稳定输出”继续走到“模型如何参考示例和外部知识回答”。

相关新闻