LangChain 核心组件之 Models

在上一篇 LangChain 核心组件之 Agent 有介绍到模型。模型其实是一个很粗泛的概念,放到任何领域都有立身之地, 比如各种建模,经济增长模型,Covid 感染模型等。但来到 AI 时代,会不会只要有人一开口说模型便默认为大语言模型(LLM)呢?而如今的 LLM 模型又基本就是 Transformer 模型,所谓的模型开源只是开放了一堆的 Token 的权重值,不同源软件开源,拿过来能随意定制使用。

大语言模型更像是人类知识的压缩包,可用它生成多种形式的内容,比如文本、图像、音频等。模型在生成内容的过程中,还支持下面几种形式的交互

  1. 工具调用:可以引导模型通知 Agent 调用工具,如计算,互联网搜索,API 调用等
  2. 结构化输出:模型本身就支持按照约定的规则格式化输出内容
  3. 多模态:不仅能生成文本,还能生成图像、音视频等
  4. 推理:模型可以进行推理,如数学计算、逻辑推理等,就是经常看到的 Thinking 的过程

最初在 Llama3 刚发布的年代,在使用 MCP 时还要查哪个模型支不支持工具使用,现在基本上以上特性是模型的标配了。现在几大商业模型就是 OpenAI 的 GPT, Anthropic 的 Claude,Google 的 Gemini, Musk 的 Grok 在下一梯队。国外的模型选择很清晰,反而中国的大语言模型众多, 仿佛不出个自己的大语言模型就像个互联网公司。

模型的创建

LangChain 标准模型接口来适配各种特定的模型,通过前面的学习,使用模型的方式可分三种

  1. 特定模型实现: model = ChatOpenAI(model="gpt-5.2")
  2. LangChain 统一接口: model = init_chat_model(model="ollama:gemma4:e4b")
  3. 通过 agent 间接使用模型:agent = create_agent(model="ollama:gemma4:e4b")

LangChain 通过 Provider 的方式支持几乎市面上主流的大语言模型,比如在 init_chat_modelcreate_agent 时有两种方式表明 Provider

  1. 模型前缀: init_chat_model(model="anthropic:claude-opus-4-6")anthropic 作为前缀表明使用 Anthropic 的模型
  2. 显示指定 provider: init_chat_model(model="gemma4:e4b", provider="ollama")provider 参数表明使用 Ollama 的模型

LangChain 支持的 Providers and models, 源代码中找 内置支持的 Provider chat_models/base.py.

初始化模型时的参数支持有:api_key(一般用特定模型的环境变量来指定 API Key), temperature(准确性),max_tokens, timeout, max_retries.

模型的调用

调用又有 model.invoke()model.stream() 两种方式,它们与模型的交互只表示一次请求与响应,不像使用 agent.invoke()agent.stream() 能进行多次请求与响应(包括工具的调用)。model.invoke()model.stream() 请求模型的方式是一样的,只处理输出结果不同, 它们都是向模型发送 一个 JSON 格式的请求,收到的是一段一段的 JSON 响应。

1from langchain.chat_models import init_chat_model
2
3model = init_chat_model(model='ollama:gemma4:e4b')
4
5response = model.invoke("Tell me a joke.")
6print(response)

response 是一个 AIMessage 对象,包含模型生成的文本和一些元数据。比如上面的输出为

content='Why did the scarecrow win an award?\n\n...Because he was outstanding in his field!' additional_kwargs={} response_metadata={'model': 'gemma4:e4b', 'created_at': '2026-04-15T02:12:13.644736Z', 'done': True, 'done_reason': 'stop', 'total_duration': 806794583, 'load_duration': 165737750, 'prompt_eval_count': 21, 'prompt_eval_duration': 89637708, 'eval_count': 20, 'eval_duration': 544281749, 'logprobs': None, 'model_name': 'gemma4:e4b', 'model_provider': 'ollama'} id='lc_run--019d8ee9-139b-7902-922b-e2781147e4f1-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 21, 'output_tokens': 20, 'total_tokens': 41}

实际上与 Ollama 的交互是

请求:

 1POST http://127.0.0.1:11434/api/chat
 2Host: 127.0.0.1:11434
 3Accept-Encoding: gzip, deflate, zstd
 4Connection: keep-alive
 5Content-Type: application/json
 6Accept: application/json
 7User-Agent: ollama-python/0.6.1 (arm64 darwin) Python/3.13.5
 8Content-Length: 117
 9
10{"model":"gemma4:e4b","stream":true,"options":{},"messages":[{"role":"user","content":"Tell me a joke."}],"tools":[]}

响应:

 1200 OK
 2Content-Type: application/x-ndjson
 3Date: Wed, 15 Apr 2026 02:12:13 GMT
 4Transfer-Encoding: chunked
 5
 6{"model":"gemma4:e4b","created_at":"2026-04-15T02:12:13.094598Z","message":{"role":"assistant","content":"Why"},"done":false}
 7{"model":"gemma4:e4b","created_at":"2026-04-15T02:12:13.1242Z","message":{"role":"assistant","content":" did"},"done":false}
 8{"model":"gemma4:e4b","created_at":"2026-04-15T02:12:13.153612Z","message":{"role":"assistant","content":" the"},"done":false}
 9{"model":"gemma4:e4b","created_at":"2026-04-15T02:12:13.184216Z","message":{"role":"assistant","content":" scare"},"done":false}
10......
11{"model":"gemma4:e4b","created_at":"2026-04-15T02:12:13.644736Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":806794583,"load_duration":165737750,"prompt_eval_count":21,"prompt_eval_duration":89637708,"eval_count":20,"eval_duration":544281749}

为节约篇幅,省略了部分 Token chunk。它的返回类型是 Content-Type: application/x-ndjson, Transfer-Encoding: chunked, 但 model.invoke() 会把每个 chunk 汇总后返回。 下面又会重提到 model.stream() 的输出处理。

我们传一段文字 model.invoke("Tell me a joke.") 相当于是

1response = model.invoke([HumanMessage(content="Tell me a joke.")])

模型是没有记忆的,如果想让模型知道上下文,那必须把每次与模型的交互历史告诉模型,我曾经问过什么,你曾经回复过什么,现在新的问题是什么要按时序堆叠 起来形成一个列表发送给模型。

1conversation = [{"role": "user", "content": "Tell me a joke."}]
2response = model.invoke(conversation)
3
4conversation.append({"role": "assistant", "content": response.content})
5
6conversation.append({"role": "user", "content": "another none"})
7response = model.invoke(conversation)
8print(response.content)

有了这个上下文字后,模型才知道第二次问话 another none 是什么意思。第二次 model.invoke() 相当于是

1model.invoke([
2    {"role": "user", "content": "Tell me a joke."},
3    {"role": "assistant", "content": "Why did the scarecrow win an award?\n\nBecause he was outstanding in his field! 😂"},
4    {"role": "user", "content": "another none"}
5])

roleuser, assistant, 还有 system 对应于具体的实例类型,以上调用加上 SystemMessage 的话相当于

1model.invoke([
2    SystemMessage(content="You are a joke maker."),
3    HumanMessage(content="Tell me a joke."),
4    AIMessage(content="Why did the scarecrow win an award?\n\nBecause he was outstanding in his field! 😂"),
5    HumanMessage(content="another none")
6])

model 除了用 [{"role": "xxx", "content": "yyy"}][XxxMessage(content="xxx")] 的形式传递会话历史外,还可以用 list[Tuple] 的形式,代码如下

1response = model.invoke([
2    ("user", "Tell me a joke."),
3    ("assistant", "Why did the scarecrow win an award?\n\nBecause he was outstanding in his field! 😂"),
4    ("user", "another none")
5])

这就是模型的上下文。

模型的流式输出

model.stream() 其实就是把模型返回的 Content-Type: application/x-ndjson 格式的一个个 chunked 渐进的输出。 ndjsonNewline Delimited JSON, 就是一大段文本里有多个按行分隔的 JSON。从前面的模型响应看就是一个 chunk 一个 JSON, 其中对应一个 Token.

流式输出

 1from io import StringIO
 2from langchain.chat_models import init_chat_model
 3
 4model = init_chat_model(
 5    model='ollama:gemma4:e4b',
 6)
 7
 8last_message = StringIO()
 9for chunk in model.stream("tell me a joke"):
10    print(chunk.text, end="", flush=True) # AIMessageChunk
11    print(chunk.text, file=last_message, end="", flush=True)
12
13print("\n---\n")
14
15for chunk in model.stream([
16    {"role": "user", "content": "Tell me a joke."},
17    {"role": "assistant", "content": last_message.getvalue()},
18    {"role": "user", "content": "another none"}
19]):
20    print(chunk.text, end="", flush=True)

上面的代码会逐个 Token 蹦出的输出以下内容,像 ChatGPT 和 ollama run ollama:gemma4:e4b 那样的聊天效果,

1Why did the scarecrow win an award?
2
3...Because he was outstanding in his field! 🏆🌽😄
4---
5
6Why don't scientists trust atoms?
7
8...Because they make up *everything*! ⚛️😉

但是遍历了所有的 chunk 都没看到 thinking 的内容输出,从抓取的响应数据包看到在第二次带有 another one 时,模型 ollama:gemma4:e4b 进行了推理,有以下内容的输出

 1{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.044361Z","message":{"role":"assistant","content":"","thinking":"Thinking"},"done":false}
 2{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.072249Z","message":{"role":"assistant","content":"","thinking":" Process"},"done":false}
 3{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.10088Z","message":{"role":"assistant","content":"","thinking":":"},"done":false}
 4{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.1567Z","message":{"role":"assistant","content":"","thinking":"\n\n1"},"done":false}
 5{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.185115Z","message":{"role":"assistant","content":"","thinking":"."},"done":false}
 6{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.239727Z","message":{"role":"assistant","content":"","thinking":"  **"},"done":false}
 7{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.26744Z","message":{"role":"assistant","content":"","thinking":"Analyze"},"done":false}
 8{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.302577Z","message":{"role":"assistant","content":"","thinking":" the"},"done":false}
 9{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.335515Z","message":{"role":"assistant","content":"","thinking":" Request"},"done":false}
10{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.367018Z","message":{"role":"assistant","content":"","thinking":":**"},"done":false}
11{"model":"gemma4:e4b","created_at":"2026-04-15T02:27:07.401783Z","message":{"role":"assistant","content":"","thinking":" The"},"done":false}

遍历 model.stream() 时不含 thinking 的 chunk 是因为创建模型时没有带 reasoning=True 参数,代码改造为

 1from io import StringIO
 2from langchain.chat_models import init_chat_model
 3
 4model = init_chat_model(
 5    model='ollama:gemma4:e4b',
 6    reasoning=True
 7)
 8
 9last_message = StringIO()
10for chunk in model.stream("tell me a joke"):
11    print(chunk.text, end="", flush=True) # AIMessageChunk
12    print(chunk.text, file=last_message, end="", flush=True)
13
14print("\n---\n")
15stopped_thinking = False
16
17for chunk in model.stream([
18    {"role": "user", "content": "Tell me a joke."},
19    {"role": "assistant", "content": last_message.getvalue()},
20    {"role": "user", "content": "another none"}
21]):
22    thinking = chunk.additional_kwargs.get("reasoning_content", "")
23
24    if thinking:
25        print(f"\033[32m{thinking}", end="", flush=True)
26    else:
27        if stopped_thinking == False and len(chunk.text) > 0:
28            print("\n---\n")
29            stopped_thinking = True
30
31        print(f"\033[0m{chunk.text}", end="", flush=True)

init_chat_model() 不指定 reasoning 参数时,模型会自行决定是否进行推理(reasoning/thinking). 指定 reasoning=False 的话模型则不进行推理。

比如对于 model.invoke("hello") 时,init_chat_model()reasoning 参数值会影响到向模型发送的数据的 think 属性。没有 reasoning 参数,reasoning=Truereasoning=False 时发给模型的请求数据分别如下:

1{"model":"gemma4:e4b","stream":true,"options":{},"messages":[{"role":"user","content":"Hello"}],"tools":[]}
2{"model":"gemma4:e4b","stream":true,"options":{},"messages":[{"role":"user","content":"Hello"}],"tools":[],"think":true}
3{"model":"gemma4:e4b","stream":true,"options":{},"messages":[{"role":"user","content":"Hello"}],"tools":[],"think":false}

上面代码的输出为

也能通过 chunk.content_block 中的内容,如

  • [{'reasoning': 'Thinking', 'type': 'reasoning'}]: thinking 的输出
  • [{'text': 'What', 'type': 'text'}]: 实际回复的输出
  • 更多的 typetool_call_chunk

Streaming 还能用于事件,如 model.astream_events(),

批处理 model.invoke()

LangChain 提供了自动批处理调用模型,像是启动多个线程来执行 model.invoke() 一般。model.batch() 的使用方式为

1responses = model.batch([
2    "Why do parrots have colorful feathers?",
3    "How do airplanes fly?",
4    "What is quantum computing?"
5])
6for response in responses:
7    print(response)

这会有三个独立的模型请求, 完成整个 Batch 后收到对应的三个 AIMessage. Batch 也可以 Streaming, 并发请求,只要收到任何一个响应即可输出, 相应的函数是 model.batch_as_completed()

1for response in model.batch_as_completed([
2    "Why do parrots have colorful feathers?",
3    "How do airplanes fly?",
4    "What is quantum computing?"
5]):
6    print(response)

如果输入项太多,比如说 100 个输入,你可能不想 100 个请求同时发出去,那么可以配置它的并发数,如

1model.batch(
2    list_of_inputs,
3    config={'max_concurrency': 5}
4)

工具调用(Tool calling)

LangChain 的文档太过于冗余,在 Agents 一章上讲过工具调用,本章 Models 也讲,下面还有单独的 Tools, 就当时反复学习巩固吧,但侧重点还是有所不同的。

最早是叫 function calling, 现在多称为 tool calling,这两个是一样的,到后来发展到 MCP(Model Context Protocol), 到现在流行的 Agent Skills, 它们都有内在的关联。自从 Model 有了工具之后它才开始能实际干活了,否则还只是一个很聪明的知识问答库。tool calling 指 Agent 调用进程内的函数,MCP 是由 MCP Client 调用外部工具(RPC 操作),Agent Skills 让工具调用更动态化了。

Agent 加上工具与模型的交互语义上是这样的

  1. Agent 告诉模型,我这里有一些工具,它们有各自的用途描述,接收什么样的参数,如果你需要的话告诉我可调用哪个函数,并且提供相应的参数
  2. 模型如果判断需要调用哪个工具,则返回要调用的工具名称和参数
  3. Agent 完成工具调用,并把工具调用结果再次发给模型,同时也会把工具列表发给模型
  4. 模型如果还需要调用某个工具的话,再次进入 #2, #3 循环
  5. 如果模型判断不再需要调用工具的话,就可结束会话,返回最终结果

这是 LangChaincreate_agent() 会自动完成的循环,而 init_chat_model()invoke() 只进作一次请求/响应,是否要进行工具调用, 如何调用工具,并且把工具调用结果再次发送给模型的这一系列操作得自己动手。

测试代码

 1from langchain.chat_models import init_chat_model
 2from langchain.tools import tool
 3
 4@tool
 5def get_weather(location: str) -> str:
 6    """Get the weather at a location."""
 7    return f"It's sunny in {location}."
 8
 9model = init_chat_model(model="ollama:gemma4:e4b")
10
11model_with_tools = model.bind_tools([get_weather])
12
13response = model_with_tools.invoke("What's the weather like in Boston?")
14print(response.tool_calls)
15print(response.content_blocks)

最后看到 repsonse 中有 tool_callscontent_blocks 含有通知工具调用的指令

[{'name': 'get_weather', 'args': {'location': 'Boston'}, 'id': '36527d9b-4a7d-48be-ab15-fb72add8f9a6', 'type': 'tool_call'}]
[{'type': 'tool_call', 'id': '36527d9b-4a7d-48be-ab15-fb72add8f9a6', 'name': 'get_weather', 'args': {'location': 'Boston'}}]

继续模拟出与模型完整的交互过程

 1conversation = [{"role": "user", "content": "What's the weather like in Boston?"}]
 2response = model_with_tools.invoke(conversation)
 3conversation.append(response)
 4
 5for tool_call in response.tool_calls:
 6    tool_result = globals()[tool_call["name"]].invoke(tool_call["args"])
 7    conversation.append({"role": "tool", "content": tool_result, "tool_call_id": tool_call["id"]})
 8
 9response = model_with_tools.invoke(conversation)
10print(response.content)

最后输出的结果是

The weather in Boston is sunny.

如果使用 create_agent(), 会自动完成这一系列的交互过程, 这里只是用 model 再来做一次练习。

上面假定了只有一个工具调用就结整,如果调用工具把结果发送给模型,模型又返回说要进行另一个工具调用,那么就需要在循环中继续调用工具,发送结果给模型, 直到模型不再需要工具调用为止。工具调用时,返回的 AIMessage 中的 tool_call_id 要与返回工具调用结果给模型时 ToolMessagetool_call_id 相对应。

强制使用工具

model.bind_tools() 时可以强制当前对话是否使用工具, 有时候会很怪异,明明用不着工具时一强制,模型的回复就有点胡来了。方法是

1model_with_tools = model.bind_tools([tool_1, tool_2], tool_choice="any")

model.bind_tools() 是使用 tool_choice 参数,它的取值有 any, <tool_name>, 分别让模型选择任意一个工具和指定的工具。下面测试一下

 1from langchain.tools import tool
 2from langchain.chat_models import init_chat_model
 3
 4@tool
 5def tool_1(filepath: str) -> str:
 6    """read file content by filepath"""
 7    return "I'm a cat"
 8
 9@tool
10def tool_2(filepath: str, content: str) -> str:
11    """write file content by filepath and content"""
12    return "updated file " + filepath
13
14
15model = init_chat_model(
16    model='ollama:llama3.2:1b',
17)
18model_with_tools = model.bind_tools([tool_1, tool_2], tool_choice="any")
19
20response = model_with_tools.invoke("hello")
21print("content: ", response.content)
22print("tool_calls: ", response.tool_calls)
23print("content_blocks: ", response.content_blocks)

tool_choice="any" 时看模型的决定

content:
tool_calls: [{'name': 'tool_1', 'args': {'filepath': 'hello.txt'}, 'id': 'd60d136d-f908-4f30-a523-55518ed270d6', 'type': 'tool_call'}]
content_blocks: [{'type': 'tool_call', 'id': 'd60d136d-f908-4f30-a523-55518ed270d6', 'name': 'tool_1', 'args': {'filepath': 'hello.txt'}}]

问句 hello, 模型说请调用 tool_1('hello.txt'), 好像还有点相关性. 可是再跑一次相同的代码又会是下面的结果

1content:  {
2  "name": "{function read_file_content_by_filepath}",
3  "parameters": {
4    "filepath": "hello"
5  }
6}
7tool_calls:  []
8content_blocks:  [{'type': 'text', 'text': '{\n  "name": "{function read_file_content_by_filepath}",\n  "parameters": {\n    "filepath": "hello"\n  }\n}'}]

不是要强制使用工具吗?那好,hello 是吧,给我任意调 read_file_content_by_filepath("hello") 方法,这就乱套了。

换成

1model_with_tools = model.bind_tools([tool_1, tool_2], tool_choice="tool_2")

使用 ollma:llama3.2:1b 模型,多运行几次什么结果都有,有说调用 tool_2, 或说调用 tool_1 的,甚至是任何无名的工具函数。

所以在使用 tool_choice 时一定要描述清楚要模型干什么活,比如有两个类似的工具函数,问题和 tool_choice 双重约定,换成下面的代码

1model_with_tools = model.bind_tools([tool_1, tool_2], tool_choice="any")
2response = model_with_tools.invoke("use tool_1 to load file 'hello.txt', and explain content")

就清晰了,在问题不明确的情况下强制 tool_choice 使用工具时,是否有意外的结果很大程度上还由所选择的模型. 如果选择了一个更聪明的模型,即使用 tool_choice="tool_1", 但问题不明不白时,模型也会拒绝请求工具, 例如下面的代码

1model = init_chat_model(
2    model='ollama:gemma4:e4b',
3)
4model_with_tools = model.bind_tools([tool_1, tool_2], tool_choice="tool_1")
5
6response = model_with_tools.invoke("hello")
7print("content: ", response.content)
8print("tool_calls: ", response.tool_calls)
9print("content_blocks: ", response.content_blocks)

执行多次的结果总是

content: Hello! How can I help you today?
tool_calls: []
content_blocks: [{'type': 'text', 'text': 'Hello! How can I help you today?'}]

工具的并发调用

当一次询问模型会返回多个工具调用,例如问

1model_with_tools = model.bind_tools([tool_1, tool_2])
2response = model_with_tools.invoke("read file 1.txt and 2.txt, merge and summarize")

它返回的 tool_calls 就会包含多项

tool_calls: [{'name': 'tool_1', 'args': {'filepath': '1.txt'}, 'id': '8c327fe1-de8b-4570-b2f8-b921a813f2ed', 'type': 'tool_call'}, {'name': 'tool_1', 'args': {'filepath': '2.txt'}, 'id': 'f4f66d8d-9782-4afc-91e2-c107b51e4cca', 'type': 'tool_call'}]

Agent 端也可并发安排这两个工具函数的调用,有些模型 Provider 可用 model.bind_tools(tools, parallel_tool_calls=False) 禁用该特性。

Streaming 工具调用

当 Streaming 时, for chunk in model_with_tools.stream(), 工具调用对应的 chunkToolCallChunk.

结构化输出

模型可被要求按某个预设的格式输出内容,而不只是一段非结构化的文本. 格式化输出的 Schema 可以通过 Pydantic, TypedDict, 或者 JSON Schema 告诉模型。

下面分别演示

Pydantic Schema

 1from langchain.chat_models import init_chat_model
 2
 3from pydantic import BaseModel
 4
 5class Winner(BaseModel):
 6    first_name: str
 7    last_name: str
 8    birth_year: int
 9
10model = init_chat_model(
11    model='ollama:gemma4:e4b',
12)
13
14model_with_structure = model.with_structured_output(Winner)
15response = model_with_structure.invoke("Get one who won the Nobel Prize in Chemistry in 2020?")
16print("type: ", type(response), "\n", response)

返回的 response 就是一个 Winner 实例,没有额外信息

1type:  <class 'dict'> 
2 {'first_name': 'Peter', 'last_name': 'Wouter', 'birth_year': 1953}

模型还是很聪明的,看看上面向模型发送的什么请求

1{"model":"gemma4:e4b","stream":true,"options":{},"format":{"properties":{"first_name":{"title":"First Name","type":"string"},"last_name":{"title":"Last Name","type":"string"},"birth_year":{"title":"Birth Year","type":"integer"}},"required":["first_name","last_name","birth_year"],"title":"Winner","type":"object"},"messages":[{"role":"user","content":"Get one who won the Nobel Prize in Chemistry in 2020?"}],"tools":[]}

TypedDict Schema

换成 TypedDict 的形式, 定义有所不同

1from typing_extensions import TypedDict, Annotated
2
3class Winner(TypedDict):
4    first_name: Annotated[str, ...]
5    last_name: Annotated[str, ...]
6    birth_year: Annotated[int, ...]

执行结果是一样的,但发送给模型的提示词是不一样的

1{"model":"gemma4:e4b","stream":true,"options":{},"format":{"title":"Winner","description":"dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n    (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n    d = {}\n    for k, v in iterable:\n        d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n    in the keyword argument list.  For example:  dict(one=1, two=2)","type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"birth_year":{"type":"integer"}},"required":["first_name","last_name","birth_year"]},"messages":[{"role":"user","content":"Get one who won the Nobel Prize in Chemistry in 2020?"}],"tools":[]}

JSON Schema Schema

只要把 Pydantic 定义方式生成的 Schema 赋给 model.with_structured_output(json_schema, method="json_schema") 即可, 从 Pydantic 方式发送给模型的请求中拷贝出 format 中的 Schema 定义

1{"properties":{"first_name":{"title":"First Name","type":"string"},"last_name":{"title":"Last Name","type":"string"},
2"birth_year":{"title":"Birth Year","type":"integer"}},"required":["first_name","last_name","birth_year"],"title":"Winner","type":"object"}
3''')
4
5model_with_structure = model.with_structured_output(json_schema, method="json_schema")
6response = model_with_structure.invoke("Get one who won the Nobel Prize in Chemistry in 2020?")
7print("type: ", type(response), "\n", response)

一样的输出。

model.with_structured_output() 的参数说明

  1. include_raw=true, model.with_structured_output(Winner, include_raw=True),它 invoke() 之后返回的是结构是 {'raw': , 'parsed': , 'parsing_error': }
  2. method="function_calling", model.with_structured_output(Winner, method="function_calling"), 它会把 Schema 当作 tools 传给模型,相应的请求
    1 {"model":"gemma4:e4b","stream":true,"options":{},"messages":[{"role":"user","content":"Get one who won the Nobel Prize in Chemistry in 2020?"}],"tools":[{"type":"function","function":{"name":"Winner","description":"dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n    (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n    d = {}\n    for k, v in iterable:\n        d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n    in the keyword argument list.  For example:  dict(one=1, two=2)","parameters":{"type":"object","required":["first_name","last_name","birth_year"],"properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"birth_year":{"type":"integer"}}}}}]}
    2 
    这个 tools 定义与使用 TypedDict 方式的 Schema 很相似. 既然是工具调用,就要求客户端来进行调用,并返回 ToolMessage, 除非用 agent 自动化处理。
  3. method="json_mode", ``model.with_structured_output(None, method="json_mode")`
    1 model_with_structure = model.with_structured_output(None, method="json_mode")
    2 response = model_with_structure.invoke("""Get one who won the Nobel Prize in Chemistry in 2020?, \n
    3   extract first_name, last_name, and birth_year and return as json""")
    4 print("type: ", type(response), "\n", response)
    

这段代码返回的是一个 AIMessage, 其中 response.content 内容是

1type:  <class 'dict'> 
2 {'first_name': 'Peter', 'last_name': 'Wouter', 'birth_year': '1959'}

method=json_mode 时返回的数据构靠问题提示词中的描述,它底层是 response_format: json_object 实现的。

嵌套的结构

带嵌套的结构化数据,例如获得 2020 年诺贝尔化学奖获得者的信息,返回格式为 JSON 数组,每个元素包含 first_name, last_name, 和 birth_year 字段。

 1class Winner(BaseModel):
 2    first_name: str
 3    last_name: str
 4    birth_year: int
 5
 6class Winners(BaseModel):
 7    winners: list[Winner]
 8
 9model = init_chat_model(
10    model='ollama:gemma4:e4b',
11)
12
13model_with_structure = model.with_structured_output(Winners)
14response = model_with_structure.invoke("Get who won the Nobel Prize in Chemistry in 2020?")
15print("type: ", type(response), "\n", response)

得到的结果是

1type:  <class '__main__.Winners'>
2winners=[Winner(first_name='Omar', last_name='Atwater', birth_year=1944), Winner(first_name='Lewis', last_name='Brust', birth_year=1932), Winner(first_name='Joseph', last_name='Chrichton', birth_year=1932)]

其他更高级的话题

LangChain 1.1 之后,据官方文档介绍 model.profile 可看到关于模型的特性,但对于本地 Ollama 模型, model.profile 返回 None. 看下 Gemini 的模型

1model = init_chat_model(
2    model='gemini-2.5-flash',
3    model_provider='google_genai'
4)
5
6print(json.dumps(model.profile, indent=2))

看到了模型相关数据

 1{
 2  "max_input_tokens": 1048576,
 3  "max_output_tokens": 65536,
 4  "text_inputs": true,
 5  "image_inputs": true,
 6  "audio_inputs": true,
 7  "pdf_inputs": true,
 8  "video_inputs": true,
 9  "text_outputs": true,
10  "image_outputs": false,
11  "audio_outputs": false,
12  "video_outputs": false,
13  "reasoning_output": true,
14  "tool_calling": true,
15  "structured_output": true,
16  "image_url_inputs": true,
17  "image_tool_message": true,
18  "tool_choice": true
19}

有些模型 profile 数据可以覆盖的,其中一种做法是

1custom_profile = {
2    "max_input_tokens": 100_000,
3    "tool_calling": True,
4    "structured_output": True,
5    # ...
6}
7model = init_chat_model("...", profile=custom_profile)

Ollama 可用 /api/show 查看模型的详细信息

1curl -s http://localhost:11434/api/show -d '{"model":"gemma4:e4b"}' | jq .

多模态(Multimodal)

很多模型不仅仅是处理文本数据,还能处理如图片,音频,视频等数据,非文本数据可以通过 content blocks 传递给模型。从前面 Gemini 模型的 profile 看到它支持 input 的其他格式有 image, pdf, audio, video, 但没有 image, audio, 和 video 的输出。

本地 Ollama 模型没有一个能支持 LangChain 来生成图片的模型

推理(Reasoning)

现代模型越来越强,工具与推理都快成标配了,可以 reponse.content_blockschunk.additional_kwargs 中找到 Reasoning 的步骤与内容, 本文前方有介绍在控制台下如何用不同的颜色显示 Reasoning 的内容。另外,我们在创建 model 实例时可以选择是否启用推理,推理的级别 .

提示词缓存(Prompt Cache)

许多模型提供者具有隐式或显式的提示词缓存功能,用以应对重复 Token 时降低延迟。OpenAIGemini 提供了隐式的提示词缓存,自动帮助节约成本。 ChatOpenAI(通过 prompt_cache_key), Anthropic(AnthropicPromptCachingMiddleware), Gemini, 和 AWS Bedrock允许用户 手工指定缓存点。提示词缓存就像是给模型装了一个短期记忆`.

服务端工具使用(Server-side tool use)

从以往的经验工具调用都是模型的客户端完成的,模型只是告诉客户端该调用哪个工具,并且参数是什么,客户端调用工具后,发送一个相关联的 ToolMessage 给模型。但读到这里有的模型自身就能调用工具,不依赖于提示词中的 tools, 也没有 ToolMessage, 这就做服务端工具调用。比如问 "今天天气如何", 模型自己就能调用天气的 API 确定今天的天气状态,难怪中国的模型本身就有这么强大的敏感词过滤功能(应该是另一回事),不知道服务端工具调用是如何实现的。

运行在 Anthropic 基础设施上的模型就支持服务端的 web_search, code_execution, web_fetch, tool_search 这些方法。

服务端工具使用还是很有意思,有这个功能的话,某些操作都不用依赖于 MCP, 期待发现更多支持 Server-side tool use 的模型,反正 Ollama 本地模型是不行的。

服务端工具使用依赖的部署模型的基础设置实现的,就模型本身那一堆权重值是不会调用工具的,也就云上模型的 API 与模型之间加入了工具调用能力, 当有工具调用要求时不需要询问客户端,让服务端 API 去执行,然后结果送给模型,最后的结果才发送到客户端去。

限流(Rate Limiting)

主要是针对云上的模型,应该大部分都有限流设置,防止硬件资源过载或者是被人蒸馏,对使用端也能避免花钱过度。init_chat_model() 时可以用 rate_limiter 参数指定一个限流器,限流器的基类是 BaseRateLimiter, 在 LangChain 标准库中只有一个实现类 InMemoryRateLimiter, 下面是限流器的使用示例:

 1from langchain_core.rate_limiters import InMemoryRateLimiter
 2
 3rate_limiter = InMemoryRateLimiter(
 4    requests_per_second=0.1,  # 1 request every 10s
 5    check_every_n_seconds=0.1,  # Check every 100ms whether allowed to make a request
 6    max_bucket_size=1,  # Controls the maximum burst size.
 7)
 8
 9model = init_chat_model(
10    model="ollama:llama3.2:1b",
11    rate_limiter=rate_limiter  
12)
13
14question = "1+1? answer it as simple as you can"
15print(datetime.now(), model.invoke(question).content)
16print(datetime.now(), model.invoke(question).content)
17print(datetime.now(), model.invoke(question).content)
18print(datetime.now(), model.invoke(question).content)

上面的代码控制 10s 最多的一个请求,执行效果是

12026-04-16 13:31:01.987762 2.
22026-04-16 13:31:12.222312 2.
32026-04-16 13:31:22.233443 2
42026-04-16 13:31:32.247292 2.

发快了不会报异常,而是在 10 秒后才发送下一个请求。如果改为 requests_per_second=0.5,即每两秒可以发一个请求

12026-04-16 13:33:33.147593 2.
22026-04-16 13:33:35.451476 2.
32026-04-16 13:33:37.492235 2.
42026-04-16 13:33:39.538332 2.

Base URL 和代理设置

Base URL 常用到,比较要访问不在本机运行的 Ollama,就需要设定 base_url 来覆盖默认的 http://127.0.0.1:11434

1model = init_chat_model(
2    model="ollama:llama3.2:1b",
3    base_url="http://192.168.86.55:11434"
4)

或者仿真了某个 Provider 的 API, 或者通过 base_url 进行代理

1model = init_chat_model(
2    model="MODEL_NAME",
3    model_provider="openai",
4    base_url="BASE_URL",
5    api_key="YOUR_API_KEY",
6)

前面提到代理,有些模型提供都支持 proxy 设置,例如 ChatOpenAI

1from langchain_openai import ChatOpenAI
2
3model = ChatOpenAI(
4    model="gpt-4.1",
5    openai_proxy="http://proxy.example.com:8080"
6)

Ollama 没有 Proxy 相关参数,所以只有针对 Python 代码设置环境变量 http_proxy=http://127.0.0.1:9090 来代理,有代理的挟持就能 观察客户端与模型之间的通信数据。本人对 LangChain 的学习就非常依赖代理与反向代理来理解与模型的通信过程。

对数概率(Log probabilities)

有些模型可以输出每个 Token 预测的概率,像 OllamaGemma4 模型就支持

1model = init_chat_model(
2    model="ollama:gemma4:e4b",
3    reasoning = False,
4).bind(logprobs=True)
5
6response = model.invoke("what's your model name")
7print(response.content)
8print(response.response_metadata)

观察它的输出

1I am Gemma 4.
2{'logprobs': [Logprob(token='I', logprob=-0.08577388525009155, top_logprobs=None), Logprob(token=' am', logprob=-0.0008207597420550883, top_logprobs=None), Logprob(token=' Gemma', logprob=-0.012466958723962307, top_logprobs=None), Logprob(token=' ', logprob=-3.5138086218466924e-07, top_logprobs=None), Logprob(token='4', logprob=-9.579081705624048e-08, top_logprobs=None), Logprob(token='.', logprob=-0.0023099889513105154, top_logprobs=None)], 'model': 'gemma4:e4b', 'created_at': '2026-04-16T18:50:19.834211Z', 'done': True, 'done_reason': 'stop', 'total_duration': 484960458, 'load_duration': 150261667, 'prompt_eval_count': 15, 'prompt_eval_duration': 116573333, 'eval_count': 7, 'eval_duration': 201391248, 'model_name': 'gemma4:e4b', 'model_provider': 'ollama'}

Token 使用状况

有些模型支持输出当前模型调用,或多次调用上下文的 Token 使用情况,Ollama 的本地模型 Gemma4 已支持. 代码演示

 1from langchain.chat_models import init_chat_model
 2from langchain_core.callbacks import UsageMetadataCallbackHandler, get_usage_metadata_callback
 3
 4callback = UsageMetadataCallbackHandler()
 5
 6model = init_chat_model(
 7    model="ollama:gemma4:e4b",
 8    base_url='http://192.168.86.54:11434',
 9)
10
11result_1 = model.invoke("hello", config={"callbacks": [callback]})
12print(callback.usage_metadata)
13
14with get_usage_metadata_callback() as cb:
15    model.invoke("Hello")
16    model.invoke("Hello")
17    print(cb.usage_metadata)

输出来的信息是

1{'gemma4:e4b': {'input_tokens': 17, 'output_tokens': 210, 'total_tokens': 227}}
2{'gemma4:e4b': {'total_tokens': 319, 'input_tokens': 34, 'output_tokens': 285}}

使用 Ollama 时不用 Callback 也能从 response.response_metadata 中看到 token 的统计

1response = model.invoke("hello")
2print(response.response_metadata)

response.response_metadata

1{'model': 'gemma4:e4b', 'created_at': '2026-04-16T20:28:01.183718Z', 'done': True, 'done_reason': 'stop', 'total_duration': 612028791, 'load_duration': 144160541, 'prompt_eval_count': 17, 'prompt_eval_duration': 131673834, 'eval_count': 210, 'eval_duration': 328611165, 'logprobs': None, 'model_name': 'gemma4:e4b', 'model_provider': 'ollama'}

其中的 'prompt_eval_count': 17, 'eval_count': 210

输入输出大概是下面那样算出来的, 包括换行等特殊字符

1<start_of_turn>user
2hello<end_of_turn>
3<start_of_turn>model

模型调用参数

model.invoke() 时用 config:RunnableConfig 字典指定调用模型时的参数,其中一些关键属性是 run_name, tags, metadata, max_concurrency(batch 调用时用到), callbacks, recursion_limit(有许多工具时).

 1response = model.invoke(
 2    "Tell me a joke",
 3    config={
 4        "run_name": "joke_generation",      # Custom name for this run
 5        "tags": ["humor", "demo"],          # Tags for categorization
 6        "metadata": {"user_id": "123"},     # Custom metadata
 7        "callbacks": [my_callback_handler], # Callback handlers
 8        "model": "gpt-5-nano"               # Configure model
 9    }
10)

配置模型的默认值

 1first_model = init_chat_model(
 2        model="gpt-4.1-mini",
 3        temperature=0,
 4        configurable_fields=("model", "temperature", "max_tokens"),
 5        config_prefix="first",  # Useful when you have a chain with multiple models
 6)
 7
 8first_model.invoke(
 9    "what's your name",
10    config={
11        "configurable": {
12            "first_temperature": 0.5,
13            "first_max_tokens": 100,
14        }
15    },
16)

这就是 LangChain 核心组件 - Models 的内容,每章的内容真的挺多的,写在同一篇博客中,信息密度有些高。相对于 create_agent() 来说这是低级别的组件, 因为使用 init_chat_model() 基本所有操作都必须手工处理,如工具调用,短期记忆,甚至是结构化输出更更笨拙,但是了解它有助于我们理解与模型交互的实际过程。 学习到现在,该考虑用 create_agent() 来创建一个能帮自动化处理一些事情的 Agent。

永久链接 https://yanbin.blog/langchain-core-component-models/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。