Agent 开发学习笔记(四):Model:调用与接入

0 阅读24分钟

Agent 开发学习笔记(四):Model:调用与接入

前言

这是"从 LangChain 框架入门智能体开发"系列的第四篇。

前三篇分别讲了框架入门、Prompt 模板基础、Prompt 进阶。到了这一篇,正式进入 Model I/O 的中间环节——Model。

Prompt 篇解决的是"怎么把业务变量变成模型输入",Model 篇解决的是接下来的问题:输入有了,模型从哪来、怎么调、怎么配、怎么接、怎么省、怎么监控。

具体来说,本篇回答六个问题:

问题对应节
模型是什么?LLM 和 ChatModel 什么关系?1. 模型是什么:LLM 与 ChatModel
模型怎么调?一次性、流式还是批量?2. 模型怎么调:三种调用方式
模型怎么配?参数管什么?3. 模型怎么配:关键参数
模型怎么接?国内 API、本地、开源服务怎么选?4. 模型怎么接:四种接入方式
模型怎么省?重复请求怎么避免重复花钱?5. 模型怎么省:Caching 缓存
模型怎么监控?花了多少 token?6. 模型怎么监控:token 统计与 callback

阅读建议:

  • 第 1、2 节是基础,建议顺序读
  • 第 3 节是参数速查,可以跳读,用到再回来查
  • 第 4 节是重头,按你的接入场景选读对应小节
  • 第 5、6 节是工程实践,接入跑通后再看

术语约定:本篇出现的"模型"是泛称,实际覆盖 LLM 和 ChatModel 两类。消息对象、AIMessage、token 等概念第一次出现时会给出定义。

代码约定:所有代码基于 LangChain 新版接口(langchain-core + langchain-community)。涉及 API Key 的地方用环境变量读取,需要提前配置。

1. 模型是什么:LLM 与 ChatModel

Model 篇要回答的第一个问题是:LangChain 里的"模型"到底指什么。

进入 LangChain 的 Model I/O 模块,你会看到两个名字:LLM 和 ChatModel。它们不是两个独立的东西,而是同一底层能力的两种接口形态。

1.1 两种接口,同一个底层能力

LLMChatModel
中文叫法基础文本生成模型接口对话模型接口
输入纯文本字符串结构化消息列表(HumanMessage、SystemMessage 等)
输出纯文本字符串AIMessage 对象
适用场景通用文本生成:续写、翻译、摘要多轮对话、角色设定
底层关系原生形态基于 LLM 封装的对话形态,底层依然依赖 LLM 生成文本

两者不是同一个类,但共享同一个底层能力。LLM 是能力的基石,ChatModel 是面向对话场景的便捷封装。LangChain 用"一实一虚"来标注这层关系:LLM 是实线主路径,ChatModel 是虚线,表示它走的是同一套 Predict 逻辑,只是包装了一层对话格式。

帮助理解的一句话:

  • LLM 是"通用接口",对话、续写、翻译都能用
  • ChatModel 是"对话专用接口",专门为多轮对话、角色设定优化,底层还是调用 LLM 的能力

实际开发中,你更常打交道的会是 ChatModel,因为它直接对应 GPT-4、Claude、GLM-4 这些对话模型。但理解 LLM 是它的底层,能帮你在后面看自定义封装时想清楚:为什么继承的是 LLM 基类,而不是 ChatModel。

1.2 消息对象

ChatModel 的输入不是字符串,而是消息对象组成的列表。这是它和 LLM 最直接的区别。

消息对象是 LangChain 定义的标准格式,用来统一不同模型供应商的对话输入。不管底层是 OpenAI、Claude 还是 GLM,到了 LangChain 这一层,都表示成同一种消息对象。

LangChain 内置了五种消息类型:

消息类对应 role用途额外参数
SystemMessagesystem系统设定:身份、规则、语气、答题限制,全局生效
HumanMessageuser / human用户输入
AIMessageassistant / aiAI 回复
FunctionMessagefunction函数调用(function call)的结果name(对应函数名)
ToolMessagetool工具调用(tool call)的结果tool_call_id(对应工具 id)

函数调用和工具调用留到后续部分展开,这里只需要知道它们也是消息对象的一种。

前三类是最常用的。它们有固定角色的快捷方式:

from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

SystemMessage(content="你是专业助手,回答简洁精准")
HumanMessage(content="你好")
AIMessage(content="你好,有什么可以帮你?")

content 是核心字段,放真实文本。除此之外,AIMessage 还自带两个固定字段:

字段内容
content核心回答内容
additional_kwargs工具调用、角色等附加信息
response_metadata接口返回元数据(token 用量、模型名、停止原因等)

注意:AIMessage 是 Python 类实例,不是纯字符串。要拿真实回答文本,用 对象.content

response = chat.invoke([HumanMessage(content="你好")])
print(response.content)   # 真实文本
print(response)           # AIMessage(content='...', ...)

除了前三种固定角色的快捷方式,LangChain 还有一个底层基类 ChatMessage,允许任意 role:

from langchain_core.messages import ChatMessage

# 以下等价
SystemMessage(content="你是助手")
ChatMessage(content="你是助手", role="system")

HumanMessage(content="你好")
ChatMessage(content="你好", role="user")

AIMessage(content="你好")
ChatMessage(content="你好", role="assistant")

# 自定义角色,只能用 ChatMessage
ChatMessage(content="愿上帝与你同在", role="Jedi")

为什么需要 ChatMessage?因为有些模型 API(如 Claude、OpenAI 的某些扩展功能)支持非标准角色名。前三种快捷方式覆盖不了这种场景,ChatMessage 可以。

三类消息统一记忆:

消息类对应角色用途
SystemMessagesystem系统设定
HumanMessageuser / human用户输入
AIMessageassistant / aiAI 回复
ChatMessage任意通用 / 自定义角色

消息对象怎么变成模型输入,涉及到消息模板的组装,详见《Prompt 模板基础》篇。

知道了模型是什么、输入长什么样,下一步看怎么调用它。


2. 模型怎么调:三种调用方式

模型接好了,接下来是让它干活。LangChain 提供了三种调用方式,对应三种不同的使用场景。

2.1 invoke:一次性返回

invoke 是最基础的调用方式:发出请求,等待模型生成完整回复,一次性拿到结果。

对于 ChatModel,输入是消息对象列表:

from langchain_community.chat_models import ChatZhipuAI
from langchain_core.messages import HumanMessage, SystemMessage
import os

chat = ChatZhipuAI(
    api_key=os.getenv("ZHIPU_API_KEY"),
    model="glm-4-flash",
)

response = chat.invoke([
    SystemMessage(content="你是专业助手,回答简洁精准"),
    HumanMessage(content="请介绍一下自己"),
])

print(response.content)

返回的是 AIMessage 对象,要拿真实文本用 .content

invoke 的特点是阻塞:代码会一直等到模型生成完最后一个 token 才继续往下走。适合不需要实时反馈的场景,比如后台任务、批量处理中的单次调用。

2.2 stream:流式返回

stream 和 invoke 的核心区别是:invoke 一次返回完整结果,stream 循环逐段返回。你需要在代码里写一个循环来接收片段。

import os
from zai import ZhipuAiClient

client = ZhipuAiClient(api_key=os.getenv("ZHIPU_API_KEY"))

response = client.chat.completions.create(
    model="glm-4-flash",
    messages=[{"role": "user", "content": "你好,请介绍一下自己"}],
    stream=True,
)

for chunk in response:
    print(chunk.choices[0].delta.content, end="")

流式返回的每个片段是 delta.content,可能是单个字符、一个词或一小段文本。用 end="" 取消换行,片段会连续打印出来,形成打字机效果。

stream 的适用场景很明确:用户会盯着屏幕等回复。比如聊天界面、代码补全工具。如果用户看不到实时输出(比如后台任务),用 stream 没有意义,反而增加代码复杂度

2.3 batch:批量返回

batch 接收一个输入列表,返回一个结果列表,顺序和输入一致。

responses = chat.batch([
    [HumanMessage(content="1+1等于几?")],
    [HumanMessage(content="2+2等于几?")],
    [HumanMessage(content="3+3等于几?")],
])

for r in responses:
    print(r.content)

batch 和 for 循环 invoke 的区别在于:batch 的默认实现是用并行方式跑多个 invoke——三个请求可能同时发出去,而不是等第一个完成再发第二个。

但这不意味着 batch 总是更快。它有一个延迟开销:需要收集所有输入、一次性发送、等待所有结果返回。对于少量输入(比如 1-2 个),用 invoke 循环反而更直接。通常 10 个以上才值得考虑。

batch 的价值在输入数量多、且彼此独立的场景——比如翻译 100 句话、对 50 条评论做情感分析。输入量大时,它能自动并行发起多个 invoke,而不是等上一个完成再发下一个。但并行度需要控制,可以用 max_concurrency 限制同时进行的请求数,避免触发 API 限流。

responses = chat.batch(inputs, config={"max_concurrency": 5})

2.4 对比表

方式输入返回特点适用场景
invoke消息对象列表AIMessage阻塞,一次拿到完整结果单次请求,用户等待时间可接受
stream消息对象列表逐个 chunk(delta.content)循环接收,实时输出用户盯着屏幕看输出
batch多个消息对象列表的列表AIMessage 列表并行调用,顺序返回大批量输入,无需实时反馈

调用方式定了,但同样的输入,模型可能给出质量差异很大的输出——接下来看参数怎么调。


3. 模型怎么配:关键参数

调用方式定了,接下来是参数。同样一句提示词,参数不同,输出质量可能差很远。

ChatModel 的关键参数有五个:max_tokenstemperaturetop_pfrequency_penaltypresence_penalty

3.1 max_tokens

控制模型一次最多输出多少 token。

说明
上限约束不能超过「模型总窗口 − 当前输入已用 token」
主动限制在剩余空间内,再设一个更小的天花板

举例:gpt-3.5-turbo-16k 总窗口 16k,输入用了 2k,剩余 14k。设 max_tokens=500,模型最多输出 500;设 14000,最多输出 14k。

设置建议:

设太小设太大不设置
回答被截断、没说完输出冗长、浪费钱、响应慢默认用满剩余空间,可能失控

注意:所有输入(历史对话、系统提示、当前问题)都算作输入 token,一并计费。

3.2 temperature

控制随机性 / 创造性。取值一般 0~2。

取值效果适合
越接近 0deterministic,回答固定、严谨代码、推理、问答
越接近 2脑洞大、随机、发散文案创作、写诗、脑洞内容

业务场景口诀:

场景建议值
写代码、数据分析、知识库问答0.1~0.3
日常聊天、文案写作0.7~1.0

3.3 top_p

核采样:从概率总和前 top_p 的词里选,压缩候选词范围。

和 temperature 是两套控制随机的方案:

取值效果
top_p=0.1只选概率极高的一小撮词,回答保守、稳定
top_p=0.9允许纳入更多低概率新词,回答更多样

行业常用搭配:一般只调 temperaturetop_p 固定 0.9 不动。两个同时乱调容易效果混乱。

3.4 frequency_penalty

频率惩罚:降低重复用词、重复句式。

针对已经反复出现过的词语加大惩罚。值越高,越讨厌老生常谈、反复叠词。作用是减少复读机、句子循环、话术重复。

3.5 presence_penalty

存在惩罚:鼓励新开话题、拓展新内容。

针对只要上文出现过的内容就轻微打压。值越高,越倾向多说新观点、新角度、不局限前文。作用是避免模型一直围着一个点绕圈,提升内容丰富度。

3.6 总表

参数作用
max_tokens限制模型最大输出长度,结合输入 token 不能超过模型上下文上限
temperature温度,控制输出随机性,越低越严谨,越高越有创造性
top_p核采样,从累计概率靠前的 token 中采样,用于控制输出多样性
frequency_penalty频率惩罚,抑制词语重复出现,减少文本复读
presence_penalty存在惩罚,弱化已有内容,鼓励模型输出新观点、新内容

参数在初始化模型时传入:

chat = ChatZhipuAI(
    model="glm-4-flash",
    api_key=os.getenv("ZHIPU_API_KEY"),
    temperature=0.3,
    max_tokens=500,
)

参数会配了,但模型从哪来?接下来看四种接入方式。


4. 模型怎么接:四种接入方式

模型从哪来?这一节覆盖四种接法:国内大模型 API、本地大模型、开源模型的 OpenAI 兼容服务,以及贯穿前三种的自定义封装规范。

4.1 接入国内大模型:GLM-4

GLM-4 是智谱 2024 年发布的新一代基座大模型,整体性能相比 GLM3 提升 60%,逼近 GPT-4,支持更长上下文、多模态、更快推理,并增强了智能体能力。

接入 GLM-4 有三种方式,由外到内依次是:原生 SDK 直连、社区封装、自定义封装。

方式一:原生 SDK 直连

不依赖 LangChain,直接用智谱官方 zai-sdk 请求接口。

pip install zai-sdk

API Key 放入系统环境变量 ZHIPU_API_KEY。如果程序报错取值为 None,大概率是环境变量未生效——新增/修改后必须重启终端 / IDE。

非流式调用:

import os
from zai import ZhipuAiClient

client = ZhipuAiClient(api_key=os.getenv("ZHIPU_API_KEY"))

response = client.chat.completions.create(
    model="glm-4-flash",
    messages=[
        {"role": "user", "content": "你好,请介绍一下自己"}
    ]
)
print(response.choices[0].message.content)

流式调用只需加 stream=True,然后循环接收:

response = client.chat.completions.create(
    model="glm-4-flash",
    messages=[{"role": "user", "content": "你好,请介绍一下自己"}],
    stream=True,
)

for chunk in response:
    print(chunk.choices[0].delta.content, end="")

原生 SDK 适合独立项目快速调用、单纯实现对话问答,上手简单、轻量化。但它无法对接 LangChain 生态的链式调用、智能体、RAG 检索等功能。要融入 LangChain,就需要后两种方式。

方式二:社区封装

LangChain 社区已经适配了智谱新接口,可以直接用 ChatZhipuAI

import os
from langchain_community.chat_models import ChatZhipuAI

chat = ChatZhipuAI(
    api_key=os.getenv("ZHIPU_API_KEY"),
    model="glm-4-flash",
)

注意:LangChain 早期内置的旧版智谱模型类已因接口从 v1 升级至 v4 全面失效。

# 失效旧导入
from langchain.llms import ZhipuAI

# 可用社区封装
from langchain_community.chat_models import ChatZhipuAI

社区封装是最省事的方式,适合快速接入已适配的模型。

方式三:自定义封装

即便已有现成封装,手动自定义封装依旧有价值:可以吃透 LangChain 大模型统一接入规范,后续对接任意本地开源模型、小众私有模型,都能复用这套代码结构。

把 GLM-4 封装成 LangChain 兼容的自定义 LLM 组件:

from typing import ClassVar, List, Dict
from langchain_core.language_models.llms import LLM
from langchain_core.messages.ai import AIMessage
import os
from zai import ZhipuAiClient

zhipuai_api_key = os.getenv("ZHIPU_API_KEY")


class ChatGLM4(LLM):
    history: ClassVar[List[Dict]] = []
    client: object = None

    def __init__(self):
        super().__init__()
        self.client = ZhipuAiClient(api_key=zhipuai_api_key)

    @property
    def _llm_type(self):
        return "ChatGLM4"

    def invoke(self, prompt, history=[]):
        if history is None:
            history = []
        history.append({"role": "user", "content": prompt})
        response = self.client.chat.completions.create(
            model="glm-4-flash",
            messages=history
        )
        result = response.choices[0].message.content
        return AIMessage(content=result)

    def _call(self, prompt, history=[]):
        return self.invoke(prompt, history)

    def stream(self, prompt, history=[]):
        if history is None:
            history = []
        history.append({"role": "user", "content": prompt})
        response = self.client.chat.completions.create(
            model="glm-4-flash",
            messages=history,
            stream=True
        )
        for chunk in response:
            yield chunk.choices[0].delta.content


llm = ChatGLM4()
res = llm.invoke('请讲一个小猫咪的笑话')
print(res.content)

关键设计点:

设计点说明
LangChain 兼容继承 LLM 基类、实现 _call 和 _llm_type(LLM 基类强制要求的抽象方法和属性),确保能被 Chain、Agent 等组件识别和调用
对话历史管理每次调用自动将用户输入追加到 history 列表,支持多轮上下文
流式与非流式双支持invoke 一次性返回完整结果,stream 逐段生成
自定义封装和社区封装的关系

两者都是把目标模型接进 LangChain,只是谁来做的问题。两条路径终点一样,都是让 GLM-4 能进 LangChain。区别只是 ChatZhipuAI 是社区写好的,ChatGLM4 是你自己写的。

社区封装自定义封装
谁写LangChain 社区 / 官方你自己
目标把某个模型接进 LangChain把某个模型接进 LangChain
底层依赖该模型的官方 SDK(如 zai-sdk)该模型的官方 SDK(如 zai-sdk)
结果ChatZhipuAI 类你的 ChatGLM4 类
关系别人替你写好了你自己写

4.2 接入本地大模型:ChatGLM3-6B

本地模型和 API 模型的核心差异在推理方式:本地模型通过 transformers 库加载模型权重文件,在本地硬件上执行前向推理;API 模型通过 HTTP 请求调用远程服务。

封装本地模型时,有哪些必须写、哪些推荐写、哪些不需要写:

类别内容
必须写继承 LLM 基类;_llm_type 属性;_call() 方法
推荐写load_model() 加载本地模型文件;stream() 流式输出
不需要全写类属性(max_tokentemperature 等)可以不写,用默认;构造函数 __init__ 空着也能跑;不是所有参数都要定义,按需写即可

完整封装代码:

from langchain_core.language_models.llms import LLM
from transformers import AutoTokenizer, AutoModel
from langchain_core.messages.ai import AIMessage
from typing import ClassVar, List, Dict


class ChatGLM3(LLM):
    max_token: int = 8192
    do_sample: bool = True
    temperature: float = 0.3
    top_p: float = 0.0
    tokenizer: object = None
    model: object = None
    history: ClassVar[List[Dict]] = []

    def __init__(self):
        super().__init__()

    @property
    def _llm_type(self):
        return "ChatGLM3"

    def load_model(self, modelPath=None):
        tokenizer = AutoTokenizer.from_pretrained(
            modelPath, trust_remote_code=True, use_fast=True
        )
        model = AutoModel.from_pretrained(
            modelPath, trust_remote_code=True, device_map="auto"
        )
        model = model.eval()
        self.model = model
        self.tokenizer = tokenizer

    def _call(self, prompt, config={}, history=[]):
        return self.invoke(prompt, history)

    def invoke(self, prompt, config={}, history=[]):
        if not isinstance(prompt, str):
            prompt = prompt.to_string()
        response, history = self.model.chat(
            self.tokenizer,
            prompt,
            history=history,
            do_sample=self.do_sample,
            max_length=self.max_token,
            temperature=self.temperature
        )
        self.history = history
        return AIMessage(content=response)

    def stream(self, prompt, config={}, history=[]):
        if not isinstance(prompt, str):
            prompt = prompt.to_string()
        preResponse = ""
        for response, new_history in self.model.stream_chat(self.tokenizer, prompt):
            if preResponse == "":
                result = response
            else:
                result = response[len(preResponse):]
            preResponse = response
            yield result

实例化并加载模型:

llm = ChatGLM3()
modelPath = "H:\AI\Agent\chatglm3-6b"
llm.load_model(modelPath)

调用:

llm.invoke("中国的首都是?")

流式输出:

for response in llm.stream("写一首春节的诗"):
    print(response, end="")

模型文件通常有好几个超 2GB 的文件,下载时间较长。本节可以只听不实践,理解封装结构即可。

4.3 接入开源模型的 OpenAI 兼容服务

LangChain 直接加载本地大模型耗时较长,更推荐提前加载模型并启动为后端服务,同时将接口设计为与 OpenAI 一致。这样后续就能像请求 OpenAI 接口一样调用本地模型。

把本地开源大模型部署成标准 OpenAI 风格接口,常用的三个工具:

工具定位核心特点
LM Studio桌面应用,易用图形界面下载模型、内置 OpenAI 兼容 API、本地运行保护数据隐私
vLLM高并发高性能推理框架PagedAttention 算法、吞吐量比 HuggingFace Transformers 高 24 倍
API for Open LLMs多模型统一后端支持 ChatGLM / LLaMA / MOSS 等,OpenAI 相似响应,支持 Docker 启动

启动服务后,用 ChatOpenAI 指向本地地址:

from langchain_openai import ChatOpenAI

openai_api_key = "EMPTY"
openai_api_base = "http://127.0.0.1:1234/v1"

chat = ChatOpenAI(
    openai_api_key=openai_api_key,
    openai_api_base=openai_api_base,
    temperature=0.7,
)

chat.invoke("请问2只兔子有多少条腿?")

也可以和 Prompt、Parser 组合成链:

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("请根据下面的主题写一篇小红书营销的短文: {topic}")
output_parser = StrOutputParser()

chain = prompt | chat | output_parser

chain.invoke({"topic": "康师傅绿茶"})

这里的 | 是 LCEL 的管道语法,详见《LCEL:把三件套串起来》篇。

流式输出:

for chunk in chain.stream({"topic": "康师傅绿茶"}):
    print(chunk, end="")

4.4 自定义封装的规范

前三种接入方式里,本地模型和自定义封装都要继承 LangChain 的 LLM 基类。这套规范是通用的:LangChain 不认识的目标模型 API,都需要通过相同的方式封装——本质是做一个"翻译器",把 LangChain 的标准调用翻译成目标模型能识别的请求格式,再把模型返回结果翻译回 LangChain 格式。

继承谁

from langchain_core.language_models.llms import LLM

继承 LLM 基类后,自定义模型自动继承 Runnable 特性,无需额外实现就能用 invokebatch 等标准方法。

二选一:_generate vs _call

写法说明适用
_generate(推荐)支持返回完整 LLMResult 结构体,能附带 token 消耗、停止原因、模型信息等元数据生产场景
_call(兼容)只接收字符串提示、返回字符串结果,无法获取 token 等额外信息极简场景

必写:_llm_type

无论重写哪种核心方法,都必须实现 _llm_type 属性(用 @property 装饰),返回一个自定义的模型类型字符串(如 "ollama_custom""my_local_llm"),用于 LangChain 内部识别模型类型。

调用链

现代写法(重写 _generate):

BaseLLM.invoke → LLM.generate → LLM._generate(我们重写的方法)

invoke 方法已经在 LLM 基类中实现好了,底层自动调用 generate,而 generate 又会调用我们重写的 _generate。只要实现 _generate,整个调用链路就自动打通。

旧版写法(重写 _call):

BaseLLM.invoke → LLM.generate → LLM._generate(基类默认实现) → LLM._call(我们重写的方法)

LangChain 基类已做双向兼容,两种写法都能正常运行,但优先推荐 _generate

三个误区

误区澄清
封装仅适用于本地模型错误。封装不分本地/线上,核心是"LangChain 不认识的模型 API,都需要封装"
token 消耗是 LangChain 计算的错误。token 由模型官方(如通义千问、OpenAI)计算,LangChain 仅负责提取并展示
必须同时重写 _generate 和 _call错误。二者二选一即可,基类会自动兼容另一种

4.5 四种方式对比

方式依赖适用场景代码量
国内 API社区封装 / 自定义快速接入线上模型
本地模型transformers + 自定义离线、数据隐私
开源 OpenAI 服务ChatOpenAI本地部署 + 高并发
自定义封装LLM 基类小众模型、私有部署

模型接上了,但每次调用都要花钱。接下来看怎么省。

5. 模型怎么省:Caching 缓存

LangChain 为模型提供了可选的缓存层。重复请求相同内容时,缓存可以直接返回上次的结果,不再调用模型服务商 API。原因有两个:

理由说明
节省成本重复请求相同内容时,缓存可以避免重复调用 API,从而节省费用
提高响应速度通过减少 API 调用次数来加速应用程序

5.1 InMemoryCache:内存缓存

设置缓存:

from langchain_core.globals import set_llm_cache
from langchain_core.caches import InMemoryCache

set_llm_cache(InMemoryCache())

set_llm_cache 是全局设置:一旦设置,之后所有模型调用都会走缓存,不需要给每个模型单独配置。

首次提问:

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("请根据下面的主题写一篇小红书营销的短文: {topic}")
output_parser = StrOutputParser()

chain = prompt | chat | output_parser

chain.invoke({"topic": "康师傅绿茶"})

耗时:

Wall time: 8.64 s

再次提问同样的内容:

chain.invoke({"topic": "康师傅绿茶"})

耗时:

Wall time: 2 ms

8.64s 到 2ms。具体数值因环境和模型而异,重点是数量级差异——缓存命中时几乎不耗时。

InMemoryCache 存在内存里,程序一关缓存就没了。适合调试、临时运行。

5.2 SQLiteCache:磁盘缓存

SQLite 缓存把结果存在磁盘的一个 .db 文件里,关机重启都还在。这就是磁盘缓存,也叫持久化缓存。

from langchain_community.cache import SQLiteCache

set_llm_cache(SQLiteCache(database_path=".langchain.db"))

首次提问:

chain.invoke({"topic": "旺仔小馒头"})

耗时:

Wall time: 7.65 s

再次提问:

chain.invoke({"topic": "旺仔小馒头"})

耗时:

Wall time: 3 ms

生成的 .db 文件会落在指定路径。

SQLite 缓存的优点:

优点说明
一次调用,永久生效只要 .db 文件还在,缓存就一直有效
不重复扣 token同样的问题,永远不会重复调用 API
速度快比重新调用模型快 10~100 倍
不占内存存在硬盘里,安全持久

适合开发 + 生产环境。

5.3 RedisCache

除了内存和磁盘,还有 RedisCache 这类分布式持久化方案。适合多进程、多机器共享缓存的场景。这里不展开。

5.4 对比表

缓存类型存在哪里重启程序后还在吗适合场景
InMemoryCache内存调试、临时运行
SQLiteCache磁盘文件正式使用、反复运行

省了钱,还要知道花了多少。接下来看 token 统计。

6. 模型怎么监控:token 统计与 callback

模型调用了,缓存也配了,但每次调用到底花了多少钱?这一节解决 token 统计。

先定义 token:token 是模型处理文本的最小单位。模型不直接读字符,而是把文本切成 token 再处理。输入和输出都按 token 计费。中文里一个 token 大约对应 1~2 个汉字,英文里一个 token 大约对应 3~4 个字符。

6.1 从 response_metadata 拿

第 1 节讲过,AIMessage 自带 response_metadata 字段。token 用量就在里面:

from langchain_community.chat_models import ChatZhipuAI
from langchain_core.messages import HumanMessage
import os

chat = ChatZhipuAI(
    api_key=os.getenv("ZHIPU_API_KEY"),
    model="glm-4-flash",
)

response = chat.invoke([HumanMessage(content="你好")])
print(response.response_metadata)

输出里含 token_usage

{
    'token_usage': {
        'completion_tokens': 15,
        'prompt_tokens': 13,
        'total_tokens': 28
    },
    'model_name': 'glm-4-flash',
    'finish_reason': 'stop'
}

三个字段的含义:

字段含义
prompt_tokens输入消耗的 token
completion_tokens输出消耗的 token
total_tokens总消耗,计费依据

每次调用后手动取一次,适合单次调试。

6.2 用 callback 记录

如果要在不侵入业务代码的前提下统计多次调用,用 callback。

LangChain 的 callback 系统允许你在执行生命周期的特定节点插入逻辑。用于 token 统计时,核心是 on_llm_end 事件——LLM 调用结束时触发,可以从 response.llm_output 中提取 token_usage

一个自定义的 token 统计 callback:

from langchain_core.callbacks import BaseCallbackHandler


class TokenCounter(BaseCallbackHandler):
    def __init__(self):
        self.total_prompt = 0
        self.total_completion = 0

    def on_llm_end(self, response, **kwargs):
        usage = response.llm_output.get("token_usage", {})
        self.total_prompt += usage.get("prompt_tokens", 0)
        self.total_completion += usage.get("completion_tokens", 0)
        print(f"本次调用: prompt={usage.get('prompt_tokens', 0)}, "
              f"completion={usage.get('completion_tokens', 0)}")

    def report(self):
        print(f"\n累计: prompt={self.total_prompt}, "
              f"completion={self.total_completion}, "
              f"total={self.total_prompt + self.total_completion}")


counter = TokenCounter()
chat.invoke([HumanMessage(content="你好")], config={"callbacks": [counter]})
chat.invoke([HumanMessage(content="再见")], config={"callbacks": [counter]})
counter.report()

把 TokenCounter 实例通过 config={"callbacks": [counter]} 传给每次调用,所有调用的 token 用量会自动累加。

如果你用的模型是 OpenAI,LangChain 有现成的 get_openai_callback

from langchain_community.callbacks import get_openai_callback

with get_openai_callback() as cb:
    chat.invoke([HumanMessage(content="你好")])
    print(f"Total: {cb.total_tokens}, Cost: ${cb.total_cost}")

但这是 OpenAI 专用的,GLM-4 等其他模型用不了,需要走上面的自定义 callback 方式。

callback 不止用于 token 统计。  它还能做日志、调试、追踪、性能测量等。token 统计只是其中一个用途。

6.3 误区

误区澄清
token 消耗是 LangChain 计算的错误。token 由模型官方(如智谱、OpenAI)计算,LangChain 仅从 API 返回中提取并展示

跨篇引用:LangSmith 是 LangChain 官方的追踪平台,能可视化每次调用的 token 用量、耗时、链路,详见《踩坑与调试》篇。


Model 篇到这里结束。模型是什么、怎么调、怎么配、怎么接、怎么省、怎么监控,六个问题都覆盖了。下一篇看 Parser:怎么把模型的输出变成结构化数据。

小结

六个问题,一张表

问题核心答案
模型是什么1LLM 与 ChatModel 是同一底层能力的两种接口形态;ChatModel 用消息对象作为输入输出
模型怎么调2invoke(一次性)/ stream(流式)/ batch(批量)
模型怎么配3max_tokens / temperature / top_p / frequency_penalty / presence_penalty
模型怎么接4国内 API / 本地模型 / 开源 OpenAI 服务 / 自定义封装
模型怎么省5InMemoryCache(内存)/ SQLiteCache(磁盘)/ RedisCache(分布式)
模型怎么监控6response_metadata 取 token_usage;callback 批量统计

核心概念清单

概念定义
LLM基础文本生成模型接口,输入输出均为纯字符串
ChatModel对话模型接口,输入消息对象列表,输出 AIMessage
消息对象SystemMessage / HumanMessage / AIMessage / FunctionMessage / ToolMessage / ChatMessage
AIMessageChatModel 的返回对象,真实文本在 .content,元数据在 response_metadata
invoke一次性调用,阻塞直到完整返回
stream流式调用,循环接收 chunk
batch批量调用,并行执行多个 invoke,用 max_concurrency 控制并发
自定义封装继承 LLM 基类,实现 _llm_type + _call 或 _generate,把非标准模型接进 LangChain
缓存set_llm_cache 全局设置,命中时直接返回上次结果,不调 API
token模型处理文本的最小单位,输入输出都按 token 计费

关键规律

接入方式选择

场景推荐方式
快速接入线上主流模型社区封装(如 ChatZhipuAI)
离线、数据隐私本地模型 + 自定义封装
本地部署 + 高并发OpenAI 兼容服务 + ChatOpenAI
小众模型、私有部署自定义封装

缓存选择

场景推荐
调试、临时运行InMemoryCache
正式使用、反复运行SQLiteCache
多进程、多机器共享RedisCache

参数速查

参数一句话
max_tokens限制输出长度
temperature控制随机性,越低越严谨
top_p核采样,一般固定 0.9 不动
frequency_penalty抑制词语重复
presence_penalty鼓励新观点

跨篇衔接

  • 消息对象的消息模板组装 → 《Prompt 模板基础》篇
  • LCEL 管道语法 | → 《LCEL:把三件套串起来》篇
  • LangSmith 追踪 → 《踩坑与调试》篇

下一篇

Model 篇结束,接下来是 Parser:怎么把模型的输出变成结构化数据。