深入学 LangChain 官方文档(二):Model 模型首讲
本篇对应的官方文档
- LangChain Models:支撑统一模型接口、初始化参数,以及
invoke、stream、batch三种调用合同。- ChatOpenAI integration:支撑模型实例化、OpenAI-compatible
base_url和响应字段兼容边界。- Model Studio OpenAI-compatible access:支撑千问兼容接口的产品入口。
- Model Studio Base URL:支撑 API key、区域、计费计划与服务地址必须对齐的认证边界。
本篇讲解范围
本篇集中讲清 Model 的接入职责,以及
invoke、stream、batch三种结果消费方式;Tool Calling、Structured Output、多模态与动态模型选择留给后续专题。
我们在第 01 篇用create_agent跑通了一个天气 Agent,但那段代码里最重要的能力仍然来自model。Agent 可以组织工具、状态和循环,却不能替代模型完成理解、选择与生成。模型对象怎样创建、怎样接收输入、怎样返回结果,这些都会直接决定上层 Agent 的能力和响应方式。
所以把model只理解成一个模型名称远远不够,因为真实场景中运行时,它会同时连接 provider package、API key、Base URL 和供应商模型;调用时又可能返回一次完整结果、连续增量块,或者一组独立任务的结果。接入信息和消费方式任何一处选错,应用体验都会变得别扭。
在langchain中,“统一模型接口”解决了调用形状问题,却没有把所有模型变成同一种产品。模型支持哪些输入、是否具备工具调用、能否稳定输出特定结构、响应中有哪些供应商扩展字段,仍然由具体集成和模型决定。因此,本篇一边使用统一的invoke、stream、batch,一边保留 provider 边界,避免把“代码写法相似”误解成“能力完全等价”。
本篇在学习文档时,会暂时拿掉工具循环,把 Model 单独放到桌面上。我们用同一个出行助手完成三类任务:一次获得完整建议、边生成边显示长建议、同时处理北京、上海和广州的独立问题。三条分支需要同时观察输入数量与返回形态:invoke对应一个完整回答,stream对应连续 chunk,batch对应多个结果。
三种调用方式的差异首先发生在应用怎样消费结果,而不是模型是否更聪明:invoke等到一个完整回答,stream持续交付增量 chunk,batch接收多个互不依赖的输入并返回多个结果。选型时真正要问的是“页面或任务怎样使用结果”,而不是机械比较哪个 API 更高级。
读完本篇,需要形成一条能够直接落到工程中的判断:单个完整结果什么时候用invoke,实时体验为什么选择stream,多个独立任务怎样用batch控制吞吐。至于每条输入为什么需要角色和历史,将在第 03 篇 Messages 中继续展开。
1. 同一个 Model,有两种运行位置
LangChain 官方文档说明,Model 可以在两种位置工作:独立调用,或者作为 Agent 的推理引擎。
独立调用时,路径很短:输入进入 Model,模型返回结果。翻译、摘要、分类、信息抽取以及不依赖外部工具的一次性问答,都可以先从这里开始。代码不需要创建 Agent,也不需要承担工具循环的额外概念。
不过,“独立”只表示没有 Agent harness,并不表示调用之后什么都不用管。业务代码仍要准备输入、处理返回对象、执行校验、决定是否再次调用以及把结果交给后续系统。如果独立 Model 绑定了工具,模型可以生成工具调用请求,但工具的实际执行和结果回填仍由调用方负责;换成 Agent 后,这段循环才由 harness 接管。
进入 Agent 后,Model 的生成接口并没有变成另一套 API。外层 harness 会根据运行状态反复调用模型:模型可能直接回答,也可能请求工具;工具结果回来以后,harness 再次调用同一个模型对象。第 01 篇中的create_agent(model=model, ...),正是把已经初始化好的 Model 交给 Agent 使用。
Model 接口在两种位置都负责把输入交给供应商并接回响应,区别在外层是否存在行动循环。独立调用由业务代码决定何时再问一次模型;Agent 运行则由 harness 根据状态决定是否继续。这条边界能避免两个相反的误区:简单任务不必先包装成 Agent,复杂 Agent 也不能指望 Model 自己管理工具执行和状态。
可以把相同的model想成一个推理入口。订单摘要服务调用一次,拿到AIMessage后进入字段校验;天气 Agent 可能在同一次运行里多次调用它,先请求天气工具,再根据工具结果形成最终建议。模型对象没有因为进入 Agent 就获得另一套生成能力,变化的是调用者:普通代码拥有流程控制权,harness 拥有循环推进权。
这也意味着 Model 的超时、认证和模型能力问题会同时影响两种位置。把同一个对象交给 Agent,不会绕过错误的 Base URL,也不会为不支持的模型能力自动增加兼容层。先用独立调用验证模型接入,再把它放进 Agent,通常更容易把连接问题和 Agent 行为问题分开。
两种位置能够复用同一个 Model,是因为 LangChain 在模型与应用之间提供了标准接口。但“接口统一”不等于“供应商完全相同”,下一步必须先把模型接入所需的几层信息对齐。
2. 接入模型要对齐三层信息
创建 Model 时,经常会同时看到 provider、package、model name、API key 和 Base URL。如果把这些词当成一张参数表,很容易出现“包能导入,请求却发不到正确模型”的情况。
可以把它们分成三层:
- provider 与 provider package:决定使用哪一种适配实现。本文用
langchain-openai中的ChatOpenAI,因为 Model Studio 提供 OpenAI-compatible 接口。 - API key 与 Base URL:决定请求发往哪个区域、工作空间或服务计划,并使用哪套凭据认证。
- model name:决定在该服务地址上调用哪个模型,这里固定为
qwen3.7-plus。
三层信息最终形成的是一次具体请求,而不是三个可以随意替换的标签。ChatOpenAI决定请求遵循哪套客户端协议,base_url决定请求到达哪个服务入口,API key 决定该入口如何认证当前调用者,model再由服务端解析为可用模型。前一层没有对齐,后面的生成参数就没有机会生效。
把三层放回同一次初始化时,重点不是记住箭头方向,而是定位错误发生在适配实现、认证地址还是模型标识。请求只有依次越过这些边界,才真正进入模型生成。
provider package、Base URL、API key 与 model name 共同确定一次调用的真实目标,任何一层错配都可能在模型开始生成前失败。包选错通常表现为导入或参数不兼容;密钥与地址不属于同一区域时可能返回 401;模型名在当前地址不可用时可能返回 404 或 model not found。
Model Studio 官方文档进一步区分了共享 DashScope、工作空间专属域名、试用域名和特定计划域名。API key 不仅受区域限制,还要与相同计费计划的 Base URL 配套。于是,“这把 key 在控制台里存在”并不能推出“它可以调用任意兼容地址”;接入时应从当前区域和计划反查正确端点。
OpenAI-compatible 也只说明服务提供了兼容的 API 形状。LangChain 的ChatOpenAIintegration 明确以官方 OpenAI API 规范为目标,第三方服务添加的非标准响应字段不一定会被提取或保留。本文使用文本生成、基础流式和通用调用参数,是因为它们位于双方共同支持的路径;一旦业务依赖供应商特有字段或能力,就应重新核对供应商文档和适配方式。
下面先建立一个可以被三种调用方式复用的模型对象:
importosfromlangchain_openaiimportChatOpenAI model=ChatOpenAI(model="qwen3.7-plus",api_key=os.environ["DASHSCOPE_API_KEY"],base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",temperature=0.2,timeout=30,max_retries=2,)model指向供应商模型,api_key只从环境变量读取,base_url指向兼容接口。示例使用北京公共 DashScope 地址;如果密钥来自其他区域、工作空间专用域名或订阅计划,应替换为控制台对应的 Base URL,不能把不同区域的密钥与地址混用。
这段初始化只做一次。后文的invoke、stream和batch都使用同一个对象,便于把注意力放在返回方式,而不是反复解释连接字段。
初始化集中还有一个排错价值。如果三个调用方式都在模型请求前失败,优先检查这段公共连接配置;只有其中一种返回方式出现问题,才继续检查流式消费或批量组织。把共享配置散落在每个业务函数里,字段稍有不同,就很难判断问题来自调用方式还是对象本身。
3. 模型参数要按控制边界理解
模型初始化参数看起来很多,但不需要逐个背。更实用的方式是先判断参数控制哪一层:
| 控制边界 | 常见参数 | 影响 |
|---|---|---|
| 调用目标 | model、api_key、base_url | 请求发到哪里、调用哪个模型、能否通过认证 |
| 生成行为 | temperature、输出 token 上限 | 回答随机性与最长输出 |
| 连接韧性 | timeout、max_retries | 等待多久、哪些临时失败允许重试 |
| 运行追踪 | invocation config 中的 tags、metadata、callbacks | 本次调用怎样被追踪和分类 |
temperature较低时输出通常更稳定,适合格式明确的业务回答;提高它可能带来更多变化,但不能把“创造力”简单等同于一个滑块。模型是否接受某个参数、参数有效范围以及思考模式如何配置,仍取决于具体供应商。
同一个参数名也不等于跨供应商具有完全相同的效果。LangChain 提供一组常见参数,但官方同时要求回到各 provider integration 查看额外能力和支持范围。统一接口适合建立应用的公共调用骨架;模型特有的推理、缓存、多模态或扩展响应参数,则不能只凭另一个供应商的示例直接迁移。
timeout控制一次请求最多等待多久,max_retries控制遇到可重试错误时尝试多少次。官方文档明确区分了临时错误和客户端配置错误:网络异常、429 或部分 5xx 可能重试;401、404 通常不会因为多试几次就自动恢复。
这组区别可以直接转成故障处理顺序。网络超时、限流和服务端错误具有暂时性,LangChain 会使用带抖动的指数退避重试;认证失败和目标不存在则属于请求本身有问题,继续发送相同请求没有意义。max_retries应用于前一类问题,修正 API key、Base URL 或模型名才解决后一类问题。
连接、生成和韧性参数只在各自边界生效。提高max_retries不能修复错误密钥,延长timeout也不能让不存在的模型名生效;同样,降低temperature只能改变生成倾向,不能代替输出校验。参数分层以后,排错就不必从一张长表里随机试值。
调用期的config又是另一层。tags、metadata、callbacks和run_name用于标识、跟踪或观察这一次运行,不会修改模型本身的连接目标,也不会替代提示内容。把它们与初始化参数分开,才能明确哪些设置会在对象的多次调用中复用,哪些信息只属于当前请求。
参数真正生效的顺序大致是:先用模型对象确定调用目标和客户端行为,再用输入与生成参数发起请求,遇到符合条件的临时故障时进入重试,最后把返回对象交给应用。按生命周期理解参数,比记住一张字段列表更可靠,因为每个错误都能落到一个尚未通过的阶段。
模型对象准备好以后,先看最直接的完整调用。
4.invoke适合等待一个完整结果
当业务逻辑必须拿到完整回答以后再继续,invoke是最直接的入口。比如出行助手需要生成一段简短建议,再整体写入订单备注:
response=model.invoke("请根据常见夏季出行情况,给北京游客三条简短准备建议。")print(response.text)输入进入模型对象,invoke等待供应商完成生成,最后返回一个AIMessage。本文只读取.text;返回对象还包含哪些结构化字段,将由下一篇 Messages 继续解释。
返回AIMessage而不是字符串,意味着“完整”描述的是本次模型响应已经结束,并不等于里面只有文本。业务代码此时获得一个可以继续检查和传递的消息对象;.text只是当前示例最关心的可读内容。先保留对象、在真正展示时再取文本,能避免过早丢失结束原因、用量或其他标准化信息。
invoke的优势是控制流简单:调用结束时,业务代码得到完整对象,可以立即进行存储、展示或下一步校验。它的代价也很明确:在模型生成较长内容时,用户必须等到完整响应回来,页面才有第一段可见结果。
因此,invoke不是“非流式的旧写法”,而是一种结果完整性选择。分类、短摘要、必须整体校验后才能使用的内容,都适合先获得完整结果。如果目标是让用户尽快看到正在生成的内容,调用方式就要换成增量消费。
完整结果合同也简化了失败处理。invoke成功返回后再写数据库,业务可以把“生成完成”和“持久化完成”设成两个清楚的状态;调用抛出异常时,本次结果尚未交给后续步骤。它适合必须先做整体长度检查、内容审核或结构校验的任务,因为校验器不需要面对尚未结束的半段内容。
代价是等待发生在调用边界内。模型已经生成了一部分,用户也看不到,直到完整AIMessage返回。短分类任务通常可以接受这种等待,长篇交互页面则可能让用户误以为请求没有开始。是否使用invoke,取决于后续步骤是否需要一个不可分割的完整结果,而不是只看实现代码最短。
这条时间轴把invoke的取舍落到了控制权交接上:调用开始后,业务代码一直等待;完整AIMessage返回,应用才同时获得文本和元数据,并继续整体校验或持久化。生成已经开始但首段尚不可见,不是模型没有工作,而是当前消费合同把“完整返回”设成了业务继续的条件。
当页面不能接受这段不可见的等待时,需要改变的正是交付条件:不再把完整AIMessage作为第一次反馈,而是在生成过程中持续接收增量对象。Model 仍执行同一次生成,应用接管结果的时间点却提前了。
5.stream把完整等待拆成增量 chunk
stream()返回一个迭代器,模型每产生一部分输出,应用就收到一个AIMessageChunk。终端或页面可以立即显示当前片段,不必等整段回答完成。
增量交付改变的不只是“文字出现得更早”,还增加了明确的运行状态。chunk 到达时,内容已经可见但本轮仍在生成;结束标记到达后,聚合结果才完整;连接中断时,已经显示的半段内容必须保留失败状态。模型客户端、后端、传输层和前端只要有一层把 chunk 缓冲到最后,整条链就会退化成完整等待。
同一个出行助手现在要生成较长的上海三日计划,代码可以一边显示 chunk,一边把它们聚合起来:
full_response=Noneforchunkinmodel.stream("为第一次去上海的游客制定三日行程,并解释每天的安排重点。"):print(chunk.text,end="",flush=True)full_response=chunkiffull_responseisNoneelsefull_response+chunkprint("\n\n完整结果:")print(full_response.textiffull_responseisnotNoneelse"没有收到模型输出")循环里的chunk.text适合驱动实时界面,累加后的full_response则适合保存、统计或继续传入其他处理步骤。两个用途不应该互相替代:只把片段打印到页面而不保存聚合结果,后续业务很难获得完整消息;等聚合完成后才统一显示,又失去了流式体验。
这里同时维护了两条路径:一条把当前 chunk 立刻交给终端,负责可见反馈;另一条按照到达顺序累加,负责形成最终对象。它们共享同一批增量数据,却有不同生命周期。终端上已经出现的文字是过程状态,full_response在流正常结束后才是可用于持久化和后处理的完整结果。
AIMessageChunk承担增量传输,顺序聚合后的对象承担完整结果。流式处理因此要求整条消费链都认识 chunk:模型能流式返回还不够,中间函数如果先把所有片段收齐再返回,前端仍然看不到实时更新。
所谓“整条链”至少包含模型客户端、后端业务函数、传输层和界面渲染。后端如果把迭代器转换成列表以后再返回,传输层如果只在连接结束时一次性发送,前端如果只在完成事件后更新文本,都会把流式调用重新压成一次完整等待。流式体验不是模型方法单方面提供的属性,而是每一层都保留增量语义后的结果。
聚合也不能只拼接肉眼看到的字符串。官方把 chunk 设计成可以通过相加形成完整消息,原因在于增量对象可能还承载其他标准化内容。直接累加chunk.text可以得到展示文本,却未必保留完整消息需要的全部结构;代码中的full_response = full_response + chunk更贴近消息对象的语义。
流式链路还要考虑中途失败。应用可能已经显示了一半内容,随后遇到网络中断;这时不能把半段文本悄悄当成完整答案。界面需要标记生成状态,存储层需要区分临时片段和已完成结果,必要时允许用户重试。第 13 篇会继续处理 Agent 运行中的事件流,本篇只守住 Model chunk 的边界。
重试时还要决定如何处理用户已经看到的片段。最简单的做法是把本轮标为失败,重新开始一个明确的新版本;如果直接在半段文本后追加重试结果,重复开头或语义断裂会很难识别。LangChain 提供模型调用与 chunk 对象,具体的完成标记、临时存储和界面替换策略仍属于应用状态设计。
6.batch面向多个互不依赖的输入
如果要同时生成北京、上海和广州三座城市的出行建议,连续调用三次invoke会让总等待时间叠加。batch接收一组独立输入,LangChain 默认可以并行处理它们:
questions=["给北京游客一条夏季出行建议。","给上海游客一条夏季出行建议。","给广州游客一条夏季出行建议。",]responses=model.batch(questions,config={"max_concurrency":3},)forcity,responseinzip(["北京","上海","广州"],responses):print(city,response.text)batch返回与输入相对应的一组最终结果,适合离线预处理、批量摘要和多个独立问答。max_concurrency限制客户端同时发起的调用数,用来平衡吞吐、连接资源和供应商限流压力;它不是提高供应商配额的开关。
“互不依赖”是使用batch的前提,而不只是性能建议。北京建议不需要上海结果,三条输入可以同时发出;如果第二个问题必须读取第一个问题的结论,它们就构成顺序工作流,强行并发只会让后一个请求缺少上下文。判断能否 batch,先画输入之间的依赖,再讨论并发数量。
默认batch等整批完成后返回结果列表,因此代码可以继续用输入位置配对城市。某一个请求明显更慢时,整批消费仍要等待它;这与三个请求是否在客户端并发并不矛盾——并发缩短总体处理时间,而返回合同仍然选择“一起交付”。
如果希望哪个任务先完成就先处理哪个,可以使用batch_as_completed()。这种方式的返回顺序可能与输入顺序不同,业务代码需要保留索引或任务 ID,不能再依赖列表位置直接配对。
官方的完成顺序接口会连同输入索引一起产出结果。假设广州先完成,它可能先返回索引 2;应用可以立即展示广州结果,同时用索引把它写回原任务位置。索引解决的是本次列表内的配对,真实队列还应保存稳定任务 ID,避免任务重试、分页或跨进程处理后只剩下脆弱的位置关系。
max_concurrency也要和输入总数分开。100 个问题、并发上限为 5,表示当前最多同时进行 5 个客户端模型调用,其余输入等待空位;它不会把 100 个请求变成供应商眼中的一个请求,也不会提高账户的速率或 token 配额。上限过高可能更快触发 429,过低则无法利用可用吞吐,合理值要结合供应商限制和应用资源测试。
并发调度里有三件事必须同时成立:输入之间互不依赖,任务带有可追踪索引,工作槽数量不超过应用和供应商能承受的压力。batch_as_completed()允许上海先于北京交付,但索引仍把结果归回原任务;batch()选择整批完成后按输入顺序返回。提高工作槽数量只会改变客户端并发,不会抬高账户配额。
更重要的是,LangChain 的batch通常表示客户端组织多个并行调用,不等同于供应商提供的离线 Batch API。后者往往把任务提交到队列,稍后再下载结果文件,适合更大的离线作业;两者在生命周期、计费和结果获取方式上都可能不同。
batch与 provider Batch API 分别位于客户端调用层和供应商任务层:前者在当前进程中并发调用并返回结果,后者通常创建异步离线作业。把两者混在一起,容易误判限流、完成时间和失败恢复方式。当前三城市示例规模很小,使用 LangChainbatch与并发上限已经足够。
失败恢复方式也随生命周期变化。客户端 batch 仍由当前应用进程等待各个模型调用,应用需要记录哪些输入已经成功、哪些需要重试;供应商离线 Batch API 往往拥有提交、排队、查询和下载等独立阶段。本文不展开后者,只强调不要因为方法名相同,就套用离线作业的价格、时限或结果文件假设。
到这里,三种调用方式的边界已经闭合:invoke决定何时拿到完整结果,stream决定怎样接收生成过程,batch决定多个独立输入怎样组织吞吐。最后的选择应回到业务消费方式,而不是停在 API 名称上。
总结:按照结果的消费方式选择调用入口
现在把三种出行任务放回同一个模型对象:
| 业务需要 | 调用方式 | 主要返回形态 | 需要注意的边界 |
|---|---|---|---|
| 等一个完整回答再继续 | invoke | AIMessage | 等待时间与整体校验 |
| 边生成边更新页面 | stream | 多个AIMessageChunk | 中途失败、chunk 聚合与全链路流式处理 |
| 处理多个独立输入 | batch | 多个最终响应 | 并发上限、限流与结果顺序 |
三者不是互相替代的性能档位,而是三种消费合同。invoke把完整性放在前面,stream把首段反馈时间放在前面,batch把独立输入的总体吞吐放在前面。实际系统也可以组合使用,例如批量任务内部流式消费,但复杂度会随状态管理和错误恢复一起增加。
Model 对象本身守住两类边界:初始化阶段把 provider package、API key、Base URL、模型名和参数对齐;调用阶段按照应用需要返回完整消息、增量块或结果列表。Agent 是否循环、是否调用工具,不属于 Model 自己的职责。
下一篇我们将继续追踪这些返回值,学习AIMessage为什么不只是文本,角色与历史怎样进入模型上下文,工具请求与工具结果如何保持对应,一起回到 LangChain 的基本上下文单位——Message的深入学习中去。