基础模型调用

前一篇笔记中,我们对LangChain的适用场景、生态进行了简单介绍,并提供了一个Demo例子。通过之前例子我们可以发现,LangChain构建的应用其实都是围绕着大语言模型(LLM)的周边展开的,因此调用LLM也是LangChain中最核心的操作。这篇笔记我们将介绍LangChain中基础对话模型(ChatModel)的配置和使用方式。

LLM Provider包

LangChain其实支持了大量的Provider。在LangChain 1.x版本中,LLM Provider的相关代码都被解耦拆分到了独立的模块中。第一篇笔记中我们曾安装过langchain-openai,它底层基于OpenAI官方SDK构建,支持OpenAI的LLM API和兼容OpenAI标准的大量第三方Provider,也是日常开发中最通用的Provider包。

uv add langchain-openai

实际上,一些第三方Provider可能既有自己的专用的Provider包也可以采用OpenAI兼容方式调用,专用的Provider包可能具备更好的兼容性并提供更多参数设置和功能,但缺点是如果我们要在多个Provider之间切换,不如统一用OpenAI兼容方式简单,而后者目前被更多开发者所接受。具体如何选择,就需要我们根据实际情况考虑了。

除此之外,如果你使用Anthropic的API则需要langchain-anthropic,使用Google Generative AI的API则需要langchain-google-genai,出于商业因素考虑,Anthropic和Google显然不会提供OpenAI兼容模式。

调用LLM

LangChain框架中,各种Provider包提供的对话模型类都派生自BaseChatModel类,而BaseChatModel类又派生自LangChain的Runnable类,它可以理解为LangChain框架中各种组件的统一可调用接口,其提供的invoke()方法可用于阻塞调用,stream()astream()可用于流式调用,pipe()则用于将多个组件组成“链”。有关Runnable类和LCEL我们将在后面章节详细介绍,这里我们先看调用对话模型的例子。

阻塞式调用

对于对话模型,最简单、最基础的阻塞式调用方式如下,下面代码会向LLM接口发起请求,然后阻塞等待直到获得完整输出。

from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="qwen3:30b-a3b",
    base_url="http://localhost:11434/v1/",
    api_key="dummy",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120,
    max_retries=6
)

resp = model.invoke("你好")
print(resp.content)

创建ChatOpenAI模型对象时,我们传入了许多参数:

  • model:该字段是模型ID,对于Ollama,这个名字必须和ollama list命令中列出的一致,对于其它第三方Provider,通常也会明确提供这个参数值以供我们调用。
  • base_url:OpenAI-compatible API的端点地址,也可以理解为服务端的基础路径,如果不传这个参数,LangChain默认会将其设置为OpenAI官方的接口基础地址。
  • api_key:OpenAI官方API需要鉴权,鉴权的API Key可以在这里设置,Ollama如果未开启鉴权则随便传一个值就行(但不能不传,否则会报错,这是OpenAI官方SDK的限制)。除了直接设置api_key参数,ChatOpenAI其实默认还会从环境变量中读取API Key,如果两个地方都未设置会报错。
  • temperature:温度,控制模型输出的随机性。
  • top_p:控制nucleus sampling(核采样)的参数,1表示不裁剪分布概率,如果传小于1的值表示只从累计概率前ptoken中采样,输出会更保守。
  • max_tokens:限制模型生成的最大token数。
  • timeout:单次请求的超时时间。
  • max_retries:请求失败时自动重试次数。

对话模型的invoke()可以直接传入一个输入字符串作为调用LLM的输入,调用后的返回结果是一个AIMessage对象,代表LLM的输出,其中通过content属性可以获得其中的文本结果。

流式调用

对话模型的stream()方法可以实现同步的流式调用。

from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="qwen3:30b-a3b",
    base_url="http://localhost:11434/v1/",
    api_key="dummy",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120,
    max_retries=6
)

for chunk in model.stream("你好"):
    print(chunk.content, end="", flush=True)

不过真实开发中,如果我们开发的是一个服务端程序,同步流式调用性能不如异步方式,异步流式调用需要使用astream()方法,并结合Python的asyncio在异步上下文中编写流式输出。

async for chunk in model.astream(messages):
    await websocket.send_text(chunk.content)

消息列表

我们知道LLM实际上是无状态的,它并没有什么“记忆”。在多轮对话中,LLM能够记得之前说过的话,是因为我们在应用层将对话历史全部传给了LLM。LangChain中,有SystemMessageHumanMessageAIMessageToolMessage这几种消息类型,分别对应系统提示词、人类输入、AI输出和工具调用信息。我们这里主要演示前3种消息,有关工具调用将在后面章节主要介绍。

下面例子种,演示了LLM多轮对话中的“短期记忆”,消息列表中,用户曾告诉LLM自己的名字,最后的提问则是让LLM重新输出自己的名字。

from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="qwen/qwen3-vl-30b-a3b-thinking",
    base_url="https://openrouter.ai/api/v1",
    api_key="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",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120,
    max_retries=6
)

messages = [
    SystemMessage(content="You are a helpful assistant"),
    HumanMessage(content="你好,我是Tom."),
    AIMessage(content="你好"),
    HumanMessage(content="我是谁?")
]

resp = model.invoke(messages)
print(resp.content)

模型可能会输出如下。

你是Tom,我刚刚听你说自己是Tom。有什么我可以帮你的吗?

可以看到,模型“记得”用户的名字,当然,LLM并不是真的有“记忆”,只是我们把历史消息列表发给它了。

SystemMessageHumanMessageAIMessage这些消息对象除了content还支持一些额外字段,不过大多和特定Provider相关,例如某些提供商可能支持查看历史消息日志,消息对象会支持ID参数,这些具体用到时查看相关文档即可。如果没用这些额外参数,除了直接创建消息对象,LangChain也支持一种简化写法,代码例子如下,LangChain框架内部做了一些处理,下面写法和之前是等价的。

messages = [
    {"role": "system", "content": "You are a helpful assistant"},
    {"role": "user", "content": "你好,我是Tom."},
    {"role": "ai", "content": "你好"},
    {"role": "user", "content": "我是谁?"}
]

LLM调用异常处理

调用LLM进行推理本身其实是一个成本极高的耗时操作,在等待输出过程中,各种异常都可能发生,例如连接超时、服务端接口暂时限流了、在提供商那里没用余额了等,而对于用户来说,等了半天最后却报错了,体验会非常糟糕。因此调用LLM时,加入健壮的Fallback和重试机制能让我们的AI应用用户体验更友好。

重试机制

ChatOpenAI类为例,它内置了一套重试规则,它默认针对网络错误(包括限速的429、服务器错误5xx、读超时等各种情况)进行最多6次重试,重试使用指数退避策略。不同Provider提供的对话模型类可能略有区别,主要取决于底层SDK实现和LangChain包装。

model = ChatOpenAI(
    model="qwen3:30b-a3b",
    base_url="http://localhost:11434/v1/",
    api_key="dummy",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120,
    max_retries=6
)

Fallback

Fallback能让主模型超时、限流等情况时,自动切换到备用模型。下面例子代码中,我们配置了两个OpenRouter的模型,如果第一个模型调用失败,则Fallback到第二个模型。

from langchain_openai import ChatOpenAI

openrouter_main_model = ChatOpenAI(
    model="qwen/qwen3-next-80b-a3b-instruct:free",
    base_url="https://openrouter.ai/api/v1",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120
)

openrouter_secondary_model = ChatOpenAI(
    model="qwen/qwen3-vl-30b-a3b-thinking",
    base_url="https://openrouter.ai/api/v1",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120
)

model = openrouter_main_model.with_fallbacks([openrouter_secondary_model])

resp = model.invoke("你好")
print(resp.content)

基于BaseChatModel自定义模型类

目前,绝大多数本地/私有/第三方LLM推理服务提供商都提供了OpenAI兼容的API,因此一般来说都没用理由要对接自定义LLM API,最简单、最推荐的方式就是直接用ChatOpenAI并配置自定义base_url,如果你的LLM API就是如此特殊导致不兼容ChatOpenAI,那我建议你改服务端或写一个反向代理将其包装成OpenAI兼容接口,这样它可以很方便的接入几乎所有AI框架、工具中。

如果你真的确定就是要在应用层对接一个奇怪的自定义LLM API,那么方法是继承BaseChatModel实现自定义模型类。

from langchain_core.language_models import BaseChatModel
from langchain_core.messages import AIMessage, BaseMessage
from langchain_core.outputs import ChatResult, ChatGeneration


class CustomChatModel(BaseChatModel):
    def _generate(
            self,
            messages: list[BaseMessage],
            stop: list[str] | None = None,
            **kwargs
    ) -> ChatResult:
        last_message = messages[-1].content if messages else ""
        generation = ChatGeneration(
            message=AIMessage(content=f"你说: {last_message},我收到啦!")
        )
        return ChatResult(generations=[generation])

    @property
    def _llm_type(self) -> str:
        return "my_custom_llm"


model = CustomChatModel()

resp = model.invoke("你好")
print(resp.content)

_generate()方法是BaseChatModel必须实现的生成入口,LangChain会自动调用它处理消息。不过我们这里没用真的调用LLM推理,而是直接取出消息列表的最后一条消息回显了出来。_llm_type()是模型的唯一类型标识,它是一个用@property标注的属性方法,它也是必须存在的,用于LangChain中的序列化、调试和LangSmith遥测数据收集。

基础对话模型(ChatModel) vs 智能体(Agent)

这篇笔记我们介绍了如何配置和调用基础对话模型(ChatModel),但你会发现我们的代码和第一章中的内容完全不同,因为第一章我们创建的是智能体(Agent)。ChatModel是LangChain中对LLM对话能力的基础封装。BaseChatModel是面向对话的模型基类,它以消息列表作为输入,产生AIMessage作为输出。它的核心职责就是接受输入、返回输出,你给它一段文本或消息列表,它返回一个AI生成的回复。而智能体则不同,它是构建在ChatModel之上的,以典型的reAct模式的智能体为例,它的核心是一个循环,在智能体中,LLM充当了智能决策者,它与处理输入、与工具交互、观察工具返回结果,最终才生成有意义的输出,这是一个动态决策的过程,而不是遵循固定的执行序列。

有关智能体我们将在后续章节详细介绍。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。