资讯动态

LangChain 1.x结构化输出与工具调用实战:构建可靠数据流

发布时间:2026/10/16 10:30:44 来源:尧图企业网站定制
很多朋友看完第一课之后第一反应往往是能跟模型对话了但这跟我项目里要的东西还差得远。确实从“模型聊天很流畅”到“程序能把模型的结果用起来”中间隔着一道最关键的坎——输出格式。模型默认只吐字符串而程序需要的是字典、是对象、是能直接落库和参与业务逻辑的数据。本篇教程就专门来解决这件事在 Langchain 1.x 下用结构化输出绑定强类型模型再让模型具备工具调用能力最后把两者组装成一个能自动干活的小助手。这篇内容适合已经跑通 Langchain 基础调用、想进入实战开发阶段的读者。我会用完整可运行的 Python 代码一步步演示如何在 1.x 体系下把“自由文本输出”改造成“严格数据结构”再把“查资料”的动作从模型内部挪到真实函数里最后组合成一个自动查询并输出格式化报告的小型 Agent。1. 为什么教程的第二课要讲输出与工具这两件事1.1 模型返回字符串不是终点而是麻烦的开始先用最朴素的场景感受一下。假设你已经写出这样的代码resp llm.invoke(给我查一下明天上海的天气) print(resp.content)模型可能返回“明天上海晴28度东南风三级”也可能返回“好的根据天气预报上海明天将会是一个晴天气温在28摄氏度左右风不大”。同一句话两次调用措辞完全不一样。如果你的下游逻辑是这样写的if 晴 in resp.content: print(适合出行)你很快就会遇到两种问题。第一种模型哪天心情不好把“晴”写成“天气晴朗”你的if判断直接失效。第二种句子结构变了比如“上海明天晴转多云”你的程序只看到“晴”实际上下午要变天。字符串匹配在这个场景下脆弱得离谱。真正干活的程序需要的是{city: 上海, date: 2026-06-15, condition: sunny}这种能直接进数据库、能参与计算的数据结构。这就是结构化输出存在的意义。1.2 从字符串到数据的三层理解站在实现的角度结构化输出可以拆成三层来理解。最底层是模型的推理能力。当前主流模型通过“函数调用”机制天然支持按照调用参数的形式输出 JSON。你要做的不是教模型“输出 JSON”而是告诉它“你的输出会被我当成一个函数参数来校验”然后它会自动把内容组织成标准 JSON。中间层是格式约束层。Langchain 1.x 里这一层的核心是 Pydantic 模型。你定义好数据字段、类型、枚举值、描述信息Langchain 会把 Pydantic 模型转换成 JSON Schema塞给模型。模型照着 Schema 填空返回的结果再由 Pydantic 校验一遍不符合要求就直接报错。最上层是业务语义层。比如一个Todo模型字段有title、due_date、priority。模型输出的不再是“我明天下午三点要交报告”而是一个结构化对象你的业务代码可以像操作普通数据一样处理它。这条链路里最容易被忽略又最关键的是字段描述。模型对字段名的理解完全依赖描述信息写清楚“日期格式 YYYY-MM-DD”和只写“日期”最终输出准确率会差不少。1.3 Langchain 1.x 把这两件事放在一起的原因很多教程会把结构化输出和工具调用拆成两章单独讲但 Langchain 1.x 里这两个功能在底层其实是同一套机制——函数调用。模型侧并没有专门的“结构化输出模式”它只是把你定义的 Pydantic 模型伪装成一个“工具”这个工具的作用是“按照 Schema 输出内容”。模型正常触发工具调用返回一段带参数结构的消息Langchain 再把参数解析成 Pydantic 对象给你。你看结构化输出其实是工具调用的特例。理解这一点特别重要。你不需要为两个功能分别学两套 API 心智模型你只需要理解“模型签发工具调用请求”这一个机制结构化输出和真正的工具调用就都通了。这也是我给系列教程安排这堂课的原因——把底层机制讲透后面学 Memory、学多 Agent 协作都会轻松很多。2. 用 Pydantic 定义模型输出格式把暗语变成明文2.1 最小可行的结构化输出代码我用的 Langchain 版本是 1.x 的最新稳定版API 以当前 1.x 为准。下面这段代码是结构化输出的完整最小示例from pydantic import BaseModel, Field from typing import Literal # 定义一个天气查询结果的数据模型 class WeatherResult(BaseModel): city: str Field(description城市名称如上海) condition: Literal[sunny, cloudy, rainy] Field( description天气状况必须从枚举值中选一个 ) temperature: float Field(description当日最高气温单位摄氏度) advice: str Field(description针对该天气给出的出行建议不超过20字) # init_chat_model 是 Langchain 1.x 推荐使用的模型统一入口 # 这里用本地/私有部署的兼容服务你也可以换成任意支持工具调用的模型 llm init_chat_model( modellocal-chat-model, temperature0.1 ) # 一步绑定结构化输出 structured_llm llm.with_structured_output(WeatherResult) result structured_llm.invoke(明天上海天气怎么样) print(result) print(type(result))运行结果大概率是这样city上海 conditionsunny temperature28.5 advice适合户外活动注意防晒 class __main__.WeatherResult你可能注意到了invoke返回的已经不是一个字符串而是一个直接的WeatherResult实例。这就是with_structured_output做的事——它把模型原生输出转换成了你定义的数据类型。这里有几个魔鬼细节。第一temperature我定义的是float模型如果输出28.5°C这种带单位带引号的字符串Pydantic 校验会直接失败。解决办法就是靠Field(description)里的描述说明“不要单位只要数字”。第二condition用了Literal枚举模型必须从给定三个值里选一个杜绝了“晴转多云”这种程序没法穷举的表达。2.2 底层发生的三件事with_structured_output(WeatherResult)这行代码背后其实发生了三件事。第一件事Langchain 把WeatherResult模型转换成 JSON Schema。转换后的结果大致长这样{ title: WeatherResult, type: object, properties: { city: {type: string, description: 城市名称如上海}, condition: {enum: [sunny, cloudy, rainy], type: string}, temperature: {type: number, description: 当日最高气温单位摄氏度}, advice: {type: string, description: 针对该天气给出的出行建议不超过20字} }, required: [city, condition, temperature, advice] }第二件事Langchain 把这个 Schema 伪装成一个内部工具名字通常叫return_output或类似名称描述固定是“返回需要交给用户的内容”。然后调用llm.bind_tools把这个伪工具绑定给模型。第三件事模型收到用户问题后并不会直接输出答案文本而是产生一个工具调用意图把 WeatherResult 的字段值填进工具参数里。Langchain 截获这个参数用 Pydantic 解析成WeatherResult实例再返回给你。整个过程你不需要写任何解析代码这正是 1.x 比老版本省心的地方。老版本里你得先在 Prompt 里手动写“请输出 JSON 格式字段包括 xxx”再调用json.loads解析遇到模型输出前后带解释文字时还得额外清洗。2.3 老方法 prompt 陷进 JSON 手动解析 对比我在公司接手过一个老项目里面还是老式的做法Prompt 里塞一大段 JSON 格式说明然后后端写了一堆解析逻辑。每次模型升级解析代码就要跟着修一次。新方式最直观的好处有三个。第一Schema 自动生成不会因为手写把某个字段拼错。第二Pydantic 校验兜底字段缺失、类型不对、枚举值非法全部在程序里直接暴露出来而不是隐藏在一段含义模糊的字符串里。第三IDE 补全友好拿到的是对象点字段就能看到类型不用猜键名。这并不是说新方式百分之百稳定。模型偶尔还是会返回不符合 Schema 的内容尤其是字段多、枚举多、约束复杂的场景。后面第五章我会专门讲怎么提高输出稳定性和失败兜底。3. 工具调用让模型可以签发“任务单”但不能随便执行3.1 一个函数如何变成模型眼里的工具结构化输出解决的是“让模型按格式输出”工具调用解决的是“让模型去执行动作”。先看最简单的工具定义from langchain_core.tools import tool # 模拟一个工具库 tool def get_weather(city: str, date: str) - str: 根据城市和日期查询实时天气。 Args: city: 城市名称如上海 date: 日期格式 YYYY-MM-DD # 这里模拟真实查询实际项目中替换成 API 调用 weather_map { (上海, 2026-06-15): 晴28度, (北京, 2026-06-15): 多云25度, } return weather_map.get((city, date), 晴26度)仅仅一个tool装饰器这个普通函数就变成了模型可以调用的工具。你可以换个视角理解tool的作用它不是在“教模型调用”而是在“向模型广播这个函数的存在”。装饰器会读取函数的签名、参数名、类型注解、docstring全部转录成工具 Schema。模型看到的工具定义长这样{ name: get_weather, description: 根据城市和日期查询实时天气, parameters: { type: object, properties: { city: {type: string}, date: {type: string} }, required: [city, date] } }模型拿到这个清单之后会结合用户问题判断如果我需要天气就调用这个工具参数我负责填。填好的参数会被模型放在返回消息的tool_calls字段里。注意模型只是“提交了工具调用请求”并没有真的执行函数。3.2 bind_tools 与模型返回 ToolCall把工具与对话模型绑定的方法也很简单llm_with_tools llm.bind_tools([get_weather]) resp llm_with_tools.invoke(明天上海的天气怎么样) print(resp.content) # 空的因为模型认为需要先调用工具 print(resp.tool_calls) print(resp.additional_kwargs)这里有个新手经常困惑的点为什么resp.content是空的因为模型判断“这个问题我不能直接回答我必须先查一下天气”所以它只返回了工具调用请求没返回实际文本。resp.tool_calls的内容通常是这样的[ { name: get_weather, args: {city: 上海, date: 2026-06-15}, id: call_ab12cd..., type: tool_call } ]args就是模型替我们填好的参数。到这里模型的职责结束了接下来程序的职责开始。3.3 手动执行循环与消息闭环你需要把args交给真实的函数去执行然后把执行结果作为消息回传给模型模型才能据此生成最终回答。下面这个手动循环就是全部核心逻辑from langchain_core.messages import HumanMessage, ToolMessage def run_agent_with_tools(query: str, tools: list, llm_with_tools): messages [HumanMessage(contentquery)] resp llm_with_tools.invoke(messages) messages.append(resp) while resp.tool_calls: for tool_call in resp.tool_calls: # 找到对应的真实工具函数 matched_tool {t.name: t for t in tools}[tool_call[name]] # 执行工具函数 tool_output matched_tool.invoke(tool_call[args]) # 把工具结果以 ToolMessage 形式塞回消息列表 messages.append(ToolMessage( contenttool_output, tool_call_idtool_call[id] )) # 带着工具结果继续请求模型 resp llm_with_tools.invoke(messages) if not resp.content: messages.append(resp) else: break return resp.content answer run_agent_with_tools( 明天上海的天气怎么样适合出门吗, [get_weather], llm_with_tools ) print(answer)顺着这个代码走一遍流程就明白了。第一步用户问题进入消息列表第二步模型返回 ToolCall第三步程序执行get_weather.invoke({city: 上海, date: 2026-06-15})第四步把工具执行结果装进ToolMessage注意tool_call_id必须和模型返回的一致这是 Langchain 关联“哪次调用产生的结果”的关键第五步带着新消息再次请求模型模型现在拿到了天气数据可以生成最终回答。这个 while 循环本质上就是一个小型 Agent 运行时。你甚至可以同时传多个工具比如查询天气、查询机票、计算日期模型会自动组合调用。只要resp.tool_calls不为空循环就继续。整个机制的核心思想是模型只负责做决策不负责执行。执行永远发生在你的程序里这样任何一步出错你都看得见来龙去脉而不是把不确定的动作交给模型。4. 组合实战让模型先查数据再输出格式化报告4.1 Demo 定位与工具集现在把结构化输出和工具调用组合起来做一个有真实价值的小案例一个“活动出行助手”。用户直接说一句话比如“明天我想去上海出差帮我看看天气和不适合安排户外会议”助手需要先调用工具获取数据再输出一份结构化报告。这个案例刻意设计成“先工具、后结构化”的流程能让你看到两个特性如何在同一套消息机制里协作。工具集预备三个from pydantic import BaseModel, Field from typing import Literal, Optional from langchain_core.tools import tool tool def get_weather(city: str, date: str) - str: 查询指定城市指定日期的天气情况 return 晴气温28度东南风3级 tool def get_distance(origin: str, destination: str) - str: 查询两个城市之间的交通距离和预计耗时 return 两地距离120公里驾车预计2小时 tool def calc_days(start_date: str, end_date: str) - int: 计算两个日期之间间隔的天数参数格式YYYY-MM-DD return 5这三个工具足够覆盖一个简单的出行决策场景。注意get_weather返回的是str结构化输出的WeatherReport是BaseModel——两者并不冲突工具返回字符串给模型模型消化后再按新的 Schema 组织输出。4.2 关键代码实现与说明先定义结构化报告模型class TravelReport(BaseModel): city: str Field(description目的地城市) weather: str Field(description目的地天气概况) travel_time: str Field(description两地间交通耗时概况) recommendation: Literal[suitable, not_suitable] Field( description是否适合安排户外活动 ) reason: str Field(description给出推荐结论的理由不超过50字)绑定工具、结构化输出、组织消息循环llm_with_tools llm.bind_tools([get_weather, get_distance, calc_days]) structured_llm llm.with_structured_output(TravelReport) def run_travel_assistant(query: str) - TravelReport: messages [HumanMessage(contentquery)] resp llm_with_tools.invoke(messages) messages.append(resp) # 工具调用循环先把所有需要的数据查齐 while resp.tool_calls: for tc in resp.tool_calls: tool_map {t.name: t for t in [get_weather, get_distance, calc_days]} result tool_map[tc[name]].invoke(tc[args]) messages.append(ToolMessage(contentstr(result), tool_call_idtc[id])) resp llm_with_tools.invoke(messages) messages.append(resp) # 数据齐了让模型按 TravelReport 输出结构化结果 return structured_llm.invoke(messages) report run_travel_assistant(我明天要从北京去上海出差想安排一场户外会议帮我评估一下。) print(report)注意到一个细节我把structured_llm.invoke(messages)里的messages直接传了进去。这很重要因为模型需要阅读前面的对话历史和工具结果才能产出贴合上下文的最终报告。如果你只传一句新的问题模型就丢失了之前的工具数据可能开始瞎编。输出结果类似TravelReport( city上海, weather晴气温28度, travel_time120公里驾车约2小时, recommendationsuitable, reason天气晴朗温度适宜交通耗时较短适合安排户外会议 )这就是结构化输出和工具调用配合的完整闭环。整个流程里模型始终没有直接编造天气数据所有事实性内容都来自工具执行结果模型只负责“解读数据 按格式输出”。4.3 用 create_agent 一步到位手动 while 循环适合理解原理但实际项目里不用重复造轮子。Langchain 1.x 推荐的方式是通过内置的 Agent 工具一步到位from langgraph.prebuilt import create_agent agent create_agent( llm, tools[get_weather, get_distance, calc_days], prompt你是一个出行规划助手先调用工具获得准确的数据再回答用户问题。 ) response agent.invoke( {messages: [{role: user, content: 明天我从北京去上海出差想安排一场户外会议帮我评估一下。}]} ) print(response[messages][-1].content)create_agent把工具注册、消息循环、工具执行、异常处理都封装好了。如果模型连续多次调用工具失败它会自动停止并报错不会陷入死循环。但有一点要说清楚create_agent的默认输出是纯文本除非你在 Prompt 里指定格式或者配合 Pydantic 工具否则它不会自动生成 TravelReport 对象。如果你的最终结果要强类型对象我建议用 4.2 节那种“先手动拿工具数据再单独结构化输出”的组合方式。两种方式各有适用场景前者重灵活后者重稳定格式。5. 我在二次调试中踩过的坑和最终调参建议5.1 输出格式不稳定的常见根因你以为给了 Schema 模型就会乖乖照做但实际调参时我发现输出不稳定通常来自三个原因。第一个原因是字段描述没写清楚。get_weather的 docstring 里如果只写“查询天气”不写参数格式模型常常会传2026/06/15而不是2026-06-15。工具函数参数名写得越具体模型填对的概率越高。第二个原因是字段过多过深。当 Pydantic 模型嵌套两层以上、字段总数超过十个时模型很容易漏字段或者把类型搞错。我的经验是最多让模型承担一层结构化任务深层数据在程序里二次转换。第三个原因是温度设置过高。结构化输出场景下temperature建议调到 0.1 到 0.2 之间。越高的温度意味着越大的采样随机性模型在枚举值之间切换的几率也会变大。5.2 工具执行失败与异常回传另一个让我踩坑的点是工具函数内部异常。手动循环里如果工具函数抛了异常整个循环会直接崩溃用户什么都得不到。正确的做法是捕获异常并把错误信息回传给模型让模型想办法处理try: result tool_map[tc[name]].invoke(tc[args]) except Exception as e: result f工具执行失败: {e} messages.append(ToolMessage(contentstr(result), tool_call_idtc[id]))只要你把错误信息包装成ToolMessage回传模型通常可以自行调整回答策略比如向用户说明“天气服务暂时不可用”。这比直接崩溃要优雅得多。使用create_agent时Langgraph 内部已经做了类似处理但如果你自己写循环千万别漏掉这层保护。5.3 多轮对话中的消息污染工具调用场景里最容易出现的一个隐蔽问题在同一段对话里如果用户中途换了话题旧的工具结果仍然留在消息列表里模型可能会错误地引用旧数据。比如用户先问“北京明天天气”再问“那上海呢”。如果没有做会话隔离模型可能把北京的工具结果当成上海的来引用。解决思路有两条短会话直接把历史消息一起传入长会话还需要引入消息裁剪策略。这个主题属于进阶的 Memory 范畴我打算在下一课单独展开讲现在你只要意识到“消息列表是共享上下文不是干净的数据库”就够了。5.4 我最终保留的几条铁律调完这个案例我给自己定了几条规则分享出来供你参考。第一能用工具就不要让模型编。凡是涉及实时数据、精确计算的场景一律通过工具获取模型的职责限定在“判断用什么工具、参数怎么填、结果怎么解读”。第二工具函数的 docstring 和参数描述要像给实习生写任务书一样认真。模型对工具的理解 80% 来自文字描述而不是函数名。第三对结构化输出设上限。单个模型的输出字段控制在十到十五个以内太复杂要么拆成多个子模型要么分多次调用。第四日志里一定要记录每条ToolMessage的tool_call_id和对应工具名排查问题的时候这条信息能省下大量时间。我在实际跑这个 Demo 时最明显的感觉是一旦把“格式责任”和“事实责任”从模型身上剥离出来系统的可预测性立刻上了一个台阶。结构化输出让数据流干净工具调用让数据来源可信两者叠加之后你得到的就不再是一个“聊天玩具”而是一个真正具备业务开发基础的可靠组件。这也是我坚持在系列第二课就讲这两个主题的原因。后续不管是加记忆、做多工具编排还是搞多 Agent 协作今天的这套“模型签任务单、程序做执行、结果回流再生成”的心智模型会一直贯穿下去。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑