Week 2 -- Day 1:Model IO 与模型抽象

Day 1:Model I/O 与模型抽象

如果说 LangChain 是一套乐高积木,那么 Model I/O 就是那几块最核心的底板砖,无论你搭建的是简单的问答链还是复杂的多智能体系统,一切逻辑的起点都是"把数据交给模型,把模型的结果拿回来"。本周我们正式进入 LangChain 核心组件的深度学习,第一天的主题就是把 Model I/O 这一层吃透,理解 LangChain 如何用统一抽象屏蔽不同模型厂商的差异,掌握模型参数的调优哲学,以及学会根据任务特征动态选择合适的模型。

BaseLanguageModel:一套接口,所有模型

在 LangChain 的世界里,无论你用的是 OpenAI 的 GPT-5.5、Anthropic 的 Claude,还是通过硅基流动等第三方代理接入的国产开源模型,它们最终都被统一在 BaseLanguageModel 这一抽象基类之下。你不需要为每个厂商学习一套新的调用方式,ChatOpenAI 的实例可以 invokeChatAnthropic 的实例同样可以 invoke,换模型往往只是一行 import 的差异。这种设计背后的思想是"面向接口编程",上层 Chain 和 Agent 只依赖 BaseLanguageModel 定义的方法签名,而不关心底层到底是哪家厂商在提供推理服务。

从类型体系来看,BaseLanguageModel 之下分化为 BaseChatModelBaseLLM 两条路径。我们日常使用的是 BaseChatModel,它操作的是 BaseMessage 消息列表,天然支持 SystemMessage、HumanMessage、AIMessage 等多种角色,比老式的纯文本补全模型更加结构化。在我们的学习环境中,由于网络条件限制,使用硅基流动作为 API 代理来调用 OpenAI 兼容接口,模型选用 Qwen/Qwen3.6-35B-A3B,虽然只涉及一个具体的模型实现类,但 LangChain 的统一抽象思维依然适用,未来接入新的模型厂商时,代码的主体逻辑无需重写,交换成本被压缩到了极限。下面这段代码就是"一套接口,多种实现"的直观体现:

from langchain_core.language_models import BaseLanguageModel
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic

# 统一的 invoke 接口
llm_openai = ChatOpenAI(model="gpt-5.5", temperature=0.1)
llm_claude = ChatAnthropic(model="claude-3-5-sonnet")

第一行导入的 BaseLanguageModel 并不直接用于实例化模型,它的价值在于类型注解,当你写一个工具函数 def route(llm: BaseLanguageModel) 时,就等于声明了"这里接受任何 LangChain 兼容的模型",从而让这个函数对 OpenAI、Anthropic、甚至本地 Ollama 模型全部适用。后两行则是日常打交道最多的两个具体实现,ChatOpenAIChatAnthropic 分别封装了两大厂商的 Chat API,但你不需要分头学习两套调用方式,二者的 invoke() 签名完全一致,temperature 这类构造参数也是通用的,每个参数名和语义在两类模型上完全相同。这意味着如果你想把一个已经写好的 Chain 从 OpenAI 切换到 Claude,只需把 llm_openai 换成 llm_claude 即可,管道的其余代码一行不动,这就是抽象的力量。

参数调优:四个旋钮的艺术

模型的行为可以通过一组参数精细调控,理解每个参数的含义是写出高质量应用的前提。这其中最常被讨论也最容易被误解的就是 temperature。temperature 控制的是模型输出概率分布的"平滑度",当 temperature 趋近于 0 时,模型在每一步都选择概率最高的 token,输出变得确定且可复现,当 temperature 拉高到 0.7 甚至 0.9 时,低概率的 token 也有机会被选中,输出变得更富变化和创意。这个参数的设置没有银弹,完全取决于场景,在做 RAG 知识问答时,你希望模型的回答忠实于检索到的文档,temperature=0.0 是最安全的选择,而在写营销文案或头脑风暴时,temperature=0.8 带来的发散性反而成了优势。一个常见的误区是认为"temperature 越低越准确",实际上低 temperature 只是让输出更确定,但如果模型本身的推理能力不足以回答某个问题,temperature 调得再低也无济于事。

与 temperature 形成互补的是 top_p,也叫 nucleus sampling。它的工作方式不是平滑整个概率分布,而是直接在候选 token 集合上做截断,将 token 按概率从高到低排序,只从累积概率刚好超过 top_p 的那些 token 中抽样。当 top_p 设为 1.0 时,相当于不做任何截断,设为 0.9 时,那些概率极低的"胡言乱语 token"就被排除了。实践中通常建议 temperature 和 top_p 二选一调优,而不是同时大幅调整两者,否则会让输出行为变得难以预测。

max_tokens 看起来是一个很"无聊"的参数,不就是限制输出长度吗?但它其实在做一件非常重要的事,成本控制和安全兜底。对于一个面向终端用户的应用,你不会希望一次调用就因为模型"说个不停"而耗尽当月的 API 预算,也不会希望异常情况下返回一个 10 万 token 的无效响应。根据场景合理设置 max_tokens,可以让应用的行为更可控。最后是 frequency_penalty,它通过在损失函数中对已出现过的 token 施加惩罚来抑制重复。当你发现模型在用 RAG 做问答时反复输出同一段文档原文,或者在长文生成中出现了"复读机"现象,给 frequency_penalty 设一个 0.1 到 0.3 的值往往能立竿见影。将这四个参数汇总起来,它们在场景中的定位就变得非常清晰:

参数 作用 建议值
temperature 随机性控制 RAG: 0.0, 创作: 0.7-0.9
max_tokens 最大输出长度 按场景设定
top_p 候选词筛选 默认 1.0
frequency_penalty 减少重复 需要时 0.1-0.3

上表看起来是四个独立的概念,但落到代码里只是 ChatOpenAI 构造函数上的四个参数,调用时一行就搞定了:

# 高精度 RAG 场景:零温度 + 抑制重复
llm_rag = ChatOpenAI(model="gpt-5.5", temperature=0.0, max_tokens=1024, frequency_penalty=0.2)

# 创意写作场景:高温度 + 开放长度
llm_creative = ChatOpenAI(model="gpt-5.5", temperature=0.85, max_tokens=4096, top_p=0.95)

上面两个实例用的是同一个模型名,但参数配置截然不同,行为上也就变成了两个"角色"。前者谨慎克制,适合准确回答事实性问题。后者大胆发散,适合需要灵感和多角度的创作任务。值得留意的是 temperaturetop_p 在这个例子里没有同时大幅偏离默认值,llm_rag 保持 top_p 默认 1.0,llm_creative 虽然 temperature 拉高但 top_p 只轻微调到了 0.95,这符合"二选一为主、同时微调为辅"的最优实践。

多模型路由:让合适的模型做合适的事

理解了统一抽象和参数调优之后,自然会引出一个问题,既然不同的模型各有擅长,我能不能在一个应用里同时使用多个模型,根据任务特征动态分发?这正是 RunnableBranch 大展身手的地方。

RunnableBranch 是 LangChain 提供的条件路由组件,它的工作方式类似于编程语言中的 if-elif-else 语句链,你定义一组"谓词函数 + 执行分支"的配对,当输入数据进入 RunnableBranch 时,它会从上到下依次评估每个谓词,第一个返回 True 的谓词对应分支被执行,如果所有谓词都不满足,则走默认分支。在模型路由的场景中,谓词函数就是用来判断"这个任务应该交给哪个模型"的决策逻辑。

比如说,你可以根据输入文本的长度来路由,短平快的问答交给 GPT-5.5 处理以节省成本,超过 500 字的复杂长文本任务交给 Claude 利用其超长上下文窗口的优势。你也可以根据任务类型来路由,代码生成类任务发给经过代码微调的模型,翻译任务发给多语言能力强的模型,通用闲聊走默认分支。更精细的路由还可以结合用户等级,付费用户的请求走高性能大模型,免费用户走轻量模型。这一切都被 RunnableBranch 的条件匹配机制干净地表达出来。把上面的思路翻译成真实的 LangChain 代码,长这个样子:

from langchain_core.runnables import RunnableBranch

# 定义两个不同配置的模型
llm_fast = ChatOpenAI(model="gpt-5.5", temperature=0.0, max_tokens=512)
llm_powerful = ChatAnthropic(model="claude-4-7-sonnet", temperature=0.1, max_tokens=4096)

# 谓词函数:根据输入判断走哪个分支
def is_long_context(input_dict: dict) -> bool:
    """文本超过 500 字走高性能模型"""
    return len(input_dict["text"]) > 500

def is_code_task(input_dict: dict) -> bool:
    """代码任务走高性能模型"""
    return any(kw in input_dict["text"].lower() for kw in ["def ", "class ", "import ", "```"])

def is_complex_question(input_dict: dict) -> bool:
    """多步骤、分析类问题走高性能模型"""
    return len(input_dict["text"].split("?")) > 2 or len(input_dict["text"].split("?")) > 2

# 组装路由:从上到下匹配,第一个命中的分支被执行
router = RunnableBranch(
    (is_long_context, llm_powerful),
    (is_code_task, llm_powerful),
    (is_complex_question, llm_powerful),
    llm_fast  # 最后一个是默认分支,不需要 condition
)

# 使用:router.invoke({"text": "解释量子计算的基本原理"})

RunnableBranch 的构造函数接受若干个 (condition, runnable) 元组,最后一个参数是没有前置条件的默认分支。调用 router.invoke() 时,LangChain 会从上到下依次执行每个 condition 函数,把输入 dict 传进去,一旦某个 condition 返回 True,对应的 runnable 就会被调用,剩余分支全部跳过。如果所有条件都返回 False(比如一条短文本的闲聊消息),就走默认的 llm_fast

RunnableBranch 本身也是 Runnable 的子类,你可以把它用 | 操作符接入任何 LCEL 管道。比如在路由之后接一个 StrOutputParser() 把 AIMessage 转成纯文本,或者把整个 router 作为一个分支嵌套进更大的条件结构中。回顾第一周学过的 LCEL 管道操作符,RunnableBranch 让原本线性的管道具备了"分叉"能力,而这正是构建非平凡 Agent 的基础。

练习任务

  • 准备两份模型配置,对同一 prompt 分别调用并记录输出差异,体会不同模型在回答风格、准确性和细节丰富度上的区别
  • 实现一个基于 RunnableBranch 的模型路由器,包含至少三个分支条件(例如按文本长度、按任务语言、按复杂度评分),每个分支指向不同的模型配置
  • 选一个知识密集型问题,分别用 temperature=0.0 和 temperature=0.9 各跑 5 次,观察并记录低 temperature 下输出的一致性和高 temperature 下发散性的变化

考核点 ✅

  1. 多模型实操:提交同时调用两个不同模型配置的对比代码,输出两套结果,并简要说明差异
  2. 路由实现:提交基于 RunnableBranch 的模型路由代码,含 3 个以上分支条件,能根据输入特征将请求分发到不同模型
  3. 参数调优:将同一问题分别用 temperature=0.0 和 0.9 测试,记录 5 轮输出差异并形成对比表
  4. 接口理解:口头解释 invoke / ainvoke / stream / batch 四种调用方式的使用场景,说明各自适合的工程环境
posted @ 2026-07-04 23:49  喵叔哟  阅读(2)  评论(0)    收藏  举报