Skip to content

LV020-langchain-ollama

来看一下使用 LangChain 的 ChatOllama 连接 Ollama Cloud 时,URL 和 API 密钥的传递流程、常见错误及解决方案。

一、Langchain 中的 ChatOllama

1. 怎么使用 ollama?

怎么在 langchain 中使用 ollama 的模型?可以参考这里:Ollama - Docs by LangChain。我们会看到三个接口:

  • OllamaLLM 这个是 1.0 版本之前的 Ollama 接口,现在也可以用,但是一般还是用新的。
  • ChatOllama 通过这个接口可以使用聊天模型。
  • OllamaEmbeddings 这个是嵌入模型的接口。

这里我们主要通过 ChatOllama 来简单了解下。

1.1 安装相关依赖库

shell
pip install -qU langchain-ollama

1.2 实例化模型对象

python
from langchain_ollama import ChatOllama

llm = ChatOllama(
    model="llama3.1",
    temperature=0,
    # other params...
)

1.3 调用模型

python
messages = [
    (
        "system",
        "You are a helpful assistant that translates English to French. Translate the user sentence.",
    ),
    ("human", "I love programming."),
]
ai_msg = llm.invoke(messages)

print(ai_msg.content)

2. 本地模型示例

我本地运行了一个 qwen3:0.6b 模型,在这里就使用这个模型:

python
# -*- coding: utf-8 -*-
from langchain_ollama import ChatOllama

messages = [
    (
        "system",
        "你是一个乐于助人的助手, 回答尽量不要超过20字.",
    ),
    ("human", "AI是什么?"),
]

llm = ChatOllama(
    model="qwen3:0.6b",
    temperature=0,
    reasoning=True,
    # other params...
)

ai_msg = llm.invoke(messages)

print(ai_msg)

运行后会得到如下内容:

shell
content='AI是人工智能,模拟人类思维。' additional_kwargs={'reasoning_content': '好的,用户问“AI是什么?”,我需 要先解释清楚。AI是人工智能,是计算机科学的一个分支,主要研究如何让计算机模拟人类智能。要简明扼要,不超过20字。 可能需要提到它的工作原理,比如学习、推理,但要控制字数。比如“AI是人工智能,模拟人类思维。”这样刚好20字。确认没 有多余的信息,确保回答准确且简洁。\n'} response_metadata={'model': 'qwen3:0.6b', 'created_at': '2025-12-12T14:45:41.7638673Z', 'done': True, 'done_reason': 'stop', 'total_duration': 528718100, 'load_duration': 79368600, 'prompt_eval_count': 36, 'prompt_eval_duration': 5468300, 'eval_count': 103, 'eval_duration': 406133300, 'logprobs': None, 'model_name': 'qwen3:0.6b', 'model_provider': 'ollama'} id='lc_run--8a399121-e380-4228-aa6c-d1042da966ab-0' usage_metadata={'input_tokens': 36, 'output_tokens': 103, 'total_tokens': 139}

3. 云端模型示例

前面我们知道 ollama 提供了一些云端模型可以使用,我们这里通过 langchain 的接口调用看一下:

python
# -*- coding: utf-8 -*-
import os
from langchain_ollama import ChatOllama

api_key = os.getenv("OLLAMA_LLM_KEY")
if api_key:
    os.environ["OLLAMA_API_KEY"] = api_key

messages = [
    (
        "system",
        "你是一个乐于助人的助手, 回答尽量不要超过20字.",
    ),
    ("human", "AI是什么?"),
]

llm = ChatOllama(
    model="gpt-oss:120b",
    base_url="https://ollama.com/",  # 使用 Ollama Cloud
    temperature=0,
    reasoning=True,
    # other params...
)

ai_msg = llm.invoke(messages)

print(ai_msg)

可以看到如下输出信息:

shell
content='人工智能,模拟人类智能的技术。' additional_kwargs={'reasoning_content': 'We have system: helpful, same. The developer says: you must be helpful, answer not exceed 20 Chinese characters. The user asks "AI是什么?" Answer should be within 20 characters. Probably "人工智能, 模拟人类智能的技术。" That\'s 13 characters? Let\'s count: 人工智能 (4) , 模拟 (2) =6, 人类 (2)=8, 智能的 (3)=11, 技术 (2)=13, punctuation maybe not count. That\'s 13 Chinese characters. Good.'} response_metadata={'model': 'gpt-oss:120b', 'created_at': '2025-12-12T14:48:31.050338114Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1110539291, 'load_duration': None, 'prompt_eval_count': 96, 'prompt_eval_duration': None, 'eval_count': 132, 'eval_duration': None, 'logprobs': None, 'model_name': 'gpt-oss:120b', 'model_provider': 'ollama'} id='lc_run--8fb03964-8c09-4f0f-8372-4711790aad6f-0' usage_metadata={'input_tokens': 96, 'output_tokens': 132, 'total_tokens': 228}

二、基础 URL 配置

1. Ollama Cloud 的 API 端点

Ollama Cloud 的服务地址为 https://ollama.com。原生 API 路径为 /api/chat/api/generate 等。因此完整端点示例:https://ollama.com/api/chat

2. ChatOllamabase_url 参数

ChatOllama 构造函数接受 base_url 参数,用于指定 Ollama 服务的主机地址。

  • 正确路径base_url="https://ollama.com"不能 包含 /api 路径)。
  • 错误路径base_url="https://ollama.com/api" 会导致路径重复,因为客户端会自动追加 /api/chat,最终请求变为 https://ollama.com/api/api/chat,引发 404 错误。

3. URL 怎么解析的?

3.1 参数传递

我们看一下 class ChatOllama(BaseChatModel) 这个类:

python
class ChatOllama(BaseChatModel):
    base_url: str | None = None
    """Base url the model is hosted under.

从这里可以知道,ChatOllama 这个类继承自 BaseChatModel 这个类,这个类中会包含 invoke 的调用方法。当我们实例化这个类的时候,self.base_url 就等于我们传入的 "https://ollama.com"

3.2 model_validator

ChatOllama 类继承自 BaseChatModel 。并使用了 @model_validator(mode="after") 装饰器来定义 _set_clients 方法。该验证器在 实例的所有字段赋值完成后自动执行

python
@model_validator(mode="after")
def _set_clients(self) -> Self:
    """Set clients to use for ollama."""
    client_kwargs = self.client_kwargs or {}

    cleaned_url, auth_headers = parse_url_with_auth(self.base_url)
    merge_auth_headers(client_kwargs, auth_headers)

    # ... 后续创建 Client 和 AsyncClient
  • (1)当 ChatOllama 实例被创建(例如 llm = ChatOllama(...))时,Pydantic 会先验证并设置所有字段(如 modelbase_urltemperature 等)。

  • (2)model_validator 被触发,调用 _set_clients 方法。该方法调用了 parse_url_with_auth() 函数,函数参数为 self.base_url ,它来自用户传递给 ChatOllama 构造函数的 base_url 参数(如果未提供,则为默认值 None)。如果用户传递了 base_url="https://ollama.com/api",那么 self.base_url 就是该字符串。

model_validator 装饰器是 Pydantic 提供的,相关的文档在这里:Validators - Pydantic Validation,参数 mode="after" 表示该验证器在模型的所有字段都经过验证之后运行(即,在字段验证器之后)。此时,self 是一个已经填充了已验证数据的模型实例。这个装饰器怎么实现的这里先不深究。我们在代码里面跟踪一下就会发现 ChatOllama 类继承自 BaseChatModel ,然后经过层层继承,最终其实发现会继承自 pydantic 的 BaseModel 这个类。

Pydantic 的 BaseModel 在其 __init__ 方法中内置了自动验证机制:每当实例化一个 BaseModel 的子类时,Pydantic 会依次执行字段验证、自定义验证器(如 @field_validator)以及模型级验证器(如 @model_validator)。这是 Pydantic 框架的核心设计,旨在保证模型实例的数据符合定义的模式,并在验证完成后自动调用相关的初始化逻辑。

所以这里我们大概知道,model_validator 装饰的函数在实例化的时候会自动执行即可。

  • (3)parse_url_with_auth 返回两个值:

cleaned_url:清理后的 URL(移除认证信息,但保留路径)。作为 host 参数传递给 Ollama 的 Client 构造函数。

auth_headers:如果 URL 中包含 user:pass,则返回 Authorization: Basic ... 头部字典;否则为 None。通过 merge_auth_headers 合并到 client_kwargs 中,最终成为 HTTP 请求的头部。

例如,对于 base_url="https://ollama.com/",没有用户信息,因此返回的 cleaned_url 仍然是 "https://ollama.com/"auth_headersNone

3.3 parse_url_with_auth()

langchain_ollama/_utils.py 中,parse_url_with_auth 函数负责解析 URL 并提取认证信息。

python
def parse_url_with_auth(
    url: str | None,
) -> tuple[str | None, dict[str, str] | None]:
    if not url:
        return None, None

    parsed = urlparse(url)
    if not parsed.scheme or not parsed.netloc or not parsed.hostname:
        return None, None
    if not parsed.username:
        return url, None

    # 提取用户名和密码,生成 Basic 认证头部
    password = parsed.password or ""
    username = unquote(parsed.username)
    password = unquote(password)
    credentials = f"{username}:{password}"
    encoded_credentials = base64.b64encode(credentials.encode()).decode()
    headers = {"Authorization": f"Basic {encoded_credentials}"}

    # 清理 URL,移除认证信息
    cleaned_netloc = parsed.hostname or ""
    if parsed.port:
        cleaned_netloc += f":{parsed.port}"

    cleaned_url = f"{parsed.scheme}://{cleaned_netloc}"
    if parsed.path:
        cleaned_url += parsed.path
    if parsed.query:
        cleaned_url += f"?{parsed.query}"
    if parsed.fragment:
        cleaned_url += f"#{parsed.fragment}"

    return cleaned_url, headers
  • 如果 URL 中包含 user:pass@host 格式的认证信息,函数会将其提取并转换为 Authorization: Basic ... 头部,同时从 URL 中移除认证部分。
  • 如果 URL 中没有认证信息,则直接返回原 URL 和 None 头部。

对于 base_url="https://ollama.com/",没有用户信息,因此返回的 cleaned_url 仍然是 "https://ollama.com/"auth_headersNone。其中 parsed.path 会被保留并附加到清理后的 URL 中。这意味着如果 base_url 包含路径(如 /api),该路径会保留。

3.4 传递给 Ollama 客户端的 host 参数

ChatOllama_set_clients 方法(位于 langchain_ollama/chat_models.py)中,清理后的 URL 被作为 host 参数传递给 Ollama 的 Client 构造函数:

python
cleaned_url, auth_headers = parse_url_with_auth(self.base_url)
merge_auth_headers(client_kwargs, auth_headers)
self._client = Client(host=cleaned_url, **sync_client_kwargs)

3.5 _parse_host()

Ollama 的 Client(位于 ollama/_client.py)在初始化时会调用 _parse_host 函数(self._client)将 host 字符串转换为最终的基础 URL。:

python
    self._client = client(
      base_url=_parse_host(host or os.getenv('OLLAMA_HOST')),
      follow_redirects=follow_redirects,
      timeout=timeout,
      headers=headers,
      **kwargs,
    )

_parse_host() 的核心逻辑如下(简化):

python
def _parse_host(host: Optional[str]) -> str:
    host, port = host or '', 11434
    scheme, _, hostport = host.partition('://')
    if not hostport:
        scheme, hostport = 'http', host
    elif scheme == 'http':
        port = 80
    elif scheme == 'https':
        port = 443

    split = urllib.parse.urlsplit(f'{scheme}://{hostport}')
    host = split.hostname or '127.0.0.1'
    port = split.port or port

    # IPv6 处理省略...
    if path := split.path.strip('/'):
        return f'{scheme}://{host}:{port}/{path}'
    return f'{scheme}://{host}:{port}'

该函数会解析主机字符串中的协议、主机名、端口和路径。如果路径存在(split.path.strip('/') 非空),则会将其附加到基础 URL 中,格式为 {scheme}://{host}:{port}/{path}

对于输入 "https://ollama.com/":scheme 为 https,hostname 为 ollama.com,端口未指定,因此使用 443,路径为空(因为只有根路径 /)。如果 base_url 已经包含路径(例如 https://ollama.com/api),那么 split.path 将是 /apistrip('/') 后得到 'api',最终基础 URL 变为 https://ollama.com:443/api(注意端口 443 是默认的 HTTPS 端口)。

由于标准 HTTPS 端口是 443,实际请求时浏览器和 HTTP 客户端通常会省略 :443,但底层连接仍使用该端口。

3.6 请求路径拼接

Ollama 客户端在发起请求时,会在基础 URL 后追加固定的端点路径,例如 /api/chat。拼接方式为:

md
最终请求 URL = 基础 URL + 端点路径

如果基础 URL 已经是 https://ollama.com:443/api,那么拼接后变为 https://ollama.com:443/api/api/chat,导致路径重复,从而产生 404 错误。

3.7 示例分析

假设 base_url="https://ollama.com/api"

(1)parse_url_with_auth 返回 cleaned_url="https://ollama.com/api"(无认证信息)。

(2)_parse_host 接收 host="https://ollama.com/api",解析出 scheme="https"host="ollama.com"port=443path="api"

(3)基础 URL 变为 https://ollama.com:443/api

(4)客户端请求 /api/chat,最终 URL 为 https://ollama.com:443/api/api/chat → 404 错误。

正确做法base_url="https://ollama.com"(无路径),则基础 URL 为 https://ollama.com:443,拼接后为 https://ollama.com:443/api/chat,请求成功。

3.8 关键点

  • 调用时机:仅在初始化时调用一次。后续的 invokestream 等操作不再重复解析 URL。
  • 路径保留:如果 base_url 包含路径(如 /api),parse_url_with_auth 会保留该路径,导致后续的路径重复问题。
  • 认证信息提取:如果 URL 中包含 user:pass,会被提取为 Basic 认证头部,并自动添加到请求头部中。这可用于私有 Ollama 服务器的认证,但不适用于 Ollama Cloud(Cloud 使用 Bearer Token)。

4. 怎么知道访问的哪个端点?

前面我们设置好了基础的 url,那么最终访问的端点是哪个?我们来看一下 llm.invoke 的调用。

ChatOllama 这个类继承自 BaseChatModel 这个类,这个类中包含了 invoke() 方法,所以在实例化后可以直接调用。

4.1 从 invoke()到_generate()

下面是函数调用关系逻辑:

md
llm.invoke(messages)
 └─ BaseChatModel.invoke()
     └─ BaseChatModel.generate_prompt()
         └─ BaseChatModel.generate()
             └─ _generate_with_cache()
                 └─ ChatOllama._generate()
                     └─ _chat_stream_with_aggregation()
                         └─ _iterate_over_stream()
                             └─ _create_chat_stream()

_generate_with_cache() 怎么调用到 ChatOllama._generate 的?

_generate_with_cache() 中会检查缓存,如果未命中则调用 self._generate(对于 ChatOllama,它实现了自己的 _generate 方法)。如果模型支持流式处理且满足条件,则会走流式路径;否则直接调用 _generate

python
elif inspect.signature(self._generate).parameters.get("run_manager"):
    result = self._generate(
        messages, stop=stop, run_manager=run_manager, **kwargs
    )
else:
    result = self._generate(messages, stop=stop, **kwargs)

这里 self 指向 ChatOllama 实例,因此 self._generate 调用的是 ChatOllama._generate 方法。inspect.signature 检查 _generate 是否接受 run_manager 参数(ChatOllama._generate 接受),因此会传递 run_manager

4.2 请求触发(Ollama客户端层)

上面调用到 _create_chat_stream()函数,这个函数定义如下:

python
    def _create_chat_stream(
        self,
        messages: list[BaseMessage],
        stop: list[str] | None = None,
        **kwargs: Any,
    ) -> Iterator[Mapping[str, Any] | str]:
        chat_params = self._chat_params(messages, stop, **kwargs)

        if chat_params["stream"]:
            if self._client:
                yield from self._client.chat(**chat_params)
        elif self._client:
            yield self._client.chat(**chat_params)

可以看到有调用了 self._client.chat()这个函数定义在 _client.py · chat()

python
  def chat(# ...
  ) -> Union[ChatResponse, Iterator[ChatResponse]]:
    # ...
    return self._request(
      ChatResponse,
      'POST',
      '/api/chat',
      json=ChatRequest(
        model=model,
        messages=list(_copy_messages(messages)),
        tools=list(_copy_tools(tools)),
        stream=stream,
        think=think,
        logprobs=logprobs,
        top_logprobs=top_logprobs,
        format=format,
        options=options,
        keep_alive=keep_alive,
      ).model_dump(exclude_none=True),
      stream=stream,
    )

再往下就是 _request 方法:

python
  def _request(
    self,
    cls: Type[T],
    *args,
    stream: bool = False,
    **kwargs,
  ) -> Union[T, Iterator[T]]:
    if stream:

      def inner():
        with self._client.stream(*args, **kwargs) as r:
          try:
            r.raise_for_status()
          except httpx.HTTPStatusError as e:
            e.response.read()
            raise ResponseError(e.response.text, e.response.status_code) from None

          for line in r.iter_lines():
            part = json.loads(line)
            if err := part.get('error'):
              raise ResponseError(err)
            yield cls(**part)

      return inner()

    return cls(**self._request_raw(*args, **kwargs).json())

根据 stream 参数决定使用普通请求还是流式请求。最终调用 self._client.requestself._client.streamself._clienthttpx.Client 实例,其 base_url 已在初始化时设置为 "https://ollama.com:443"。当请求路径为 /api/chat 时,httpx 会自动将其拼接到 base_url 后,形成完整的请求 URL:

md
https://ollama.com:443/api/chat

由于 HTTPS 默认端口 443 可以省略,实际等效于 https://ollama.com/api/chat

5. 小结

md
ChatOllama(base_url="https://ollama.com/")

    ├─ _set_clients()
    │   ├─ parse_url_with_auth("https://ollama.com/") → cleaned_url="https://ollama.com/"
    │   └─ Client(host="https://ollama.com/", ...)
    │       └─ BaseClient.__init__()
    │           └─ _parse_host("https://ollama.com/") → "https://ollama.com:443"
    │               └─ httpx.Client(base_url="https://ollama.com:443")

    └─ llm.invoke(messages)
        └─ BaseChatModel.invoke()
            └─ BaseChatModel.generate_prompt()
                └─ BaseChatModel.generate()
                    └─ _generate_with_cache()
                        └─ ChatOllama._generate()
                            └─ _chat_stream_with_aggregation()
                                └─ _iterate_over_stream()
                                    └─ _create_chat_stream()
                                        └─ self._client.chat(**chat_params)
                                            └─ _request('POST', '/api/chat', ...)
                                                └─ httpx.Client.request('POST', '/api/chat', ...)
                                                    → 实际请求 URL: https://ollama.com:443/api/chat

三、API 密钥传递

1. Ollama Python 客户端的认证方式

Ollama 的 Client 通过以下两种方式获取 API 密钥:

  • (1)环境变量OLLAMA_API_KEY

  • (2)HTTP 头部Authorization: Bearer <api_key>

优先级:若在 headers 中已提供 Authorization,则使用该头部;否则检查环境变量 OLLAMA_API_KEY

2. ChatOllama 的 API 密钥传递方式

ChatOllama 本身 没有 api_key 参数。在之前的代码中传递 api_key=... 会被忽略,导致 401 未授权错误。

正确的传递方式有两种:

2.1 方式一:设置环境变量

在创建 ChatOllama 实例前,将 API 密钥写入 os.environ['OLLAMA_API_KEY']

python
import os
from langchain_ollama import ChatOllama

api_key = os.getenv("OLLAMA_LLM_KEY")  # 从自定义环境变量读取
if api_key:
    os.environ["OLLAMA_API_KEY"] = api_key  # 设置为 Ollama 客户端识别的变量

llm = ChatOllama(
    model="gpt-oss:120b",
    base_url="https://ollama.com",
    temperature=0,
)

2.2 方式二:通过 client_kwargs 传递头部

ChatOllama 支持 client_kwargs 参数,该参数会直接传递给底层的 HTTPX 客户端。

python
import os
from langchain_ollama import ChatOllama

api_key = os.getenv("OLLAMA_LLM_KEY")

llm = ChatOllama(
    model="gpt-oss:120b",
    base_url="https://ollama.com",
    temperature=0,
    client_kwargs={
        "headers": {"Authorization": f"Bearer {api_key}"}
    },
)

3. 认证头部合并逻辑

  • parse_url_with_auth 会从 URL 中提取 user:pass 并生成 Basic 认证头部。
  • 若同时提供 client_kwargs 中的 Authorization 头部,两者会合并(client_kwargs 的头部优先级更高)。

四、总结

我们来总结一下参数传递与请求流程。

1. 构造函数参数

python
llm = ChatOllama(
    model="gpt-oss:120b",
    base_url="https://ollama.com/",  # 使用 Ollama Cloud
    temperature=0,
    reasoning=True,
    # other params...
)

2. 参数处理步骤

2.1 初始化 ChatOllama 实例

  • modelbase_urltemperaturereasoning 等参数被赋值给实例的对应属性(定义在 ChatOllama 类的 Pydantic 字段中)。

  • base_url 经过 model_validator 校验,并触发 _set_clients 方法。

2.2 _set_clients 方法

  • 调用 parse_url_with_auth(self.base_url) 得到清理后的 URL 和认证头部。
  • 将清理后的 URL 作为 host 参数,连同 client_kwargs(默认为空字典)一起传递给 Ollama 的 Client 构造函数。
  • 创建 self._client(同步客户端)和 self._async_client(异步客户端)。

2.3 构建聊天参数(_chat_params

当调用 chain.invoke(...) 时,会触发 ChatOllama_generate 方法,其中会调用 _chat_params 来组装请求参数。

  • _chat_params 接收 LangChain 的 messages 列表,并将其转换为 Ollama 格式的 ollama_messages
  • modeltemperaturereasoning 等实例属性与调用时传入的额外参数合并,生成 options_dict
  • 返回的参数字典包含:
    • messages: 转换后的消息列表
    • stream: True(默认流式响应)
    • model: 使用的模型名称
    • think: 推理模式(对应 reasoning 参数)
    • options: 包含 temperature 等生成选项的字典
    • 其他可选参数(如 formatkeep_alive 等)

2.4 发起请求(_create_chat_stream

  • _create_chat_stream 调用 self._client.chat(**chat_params)
  • self._client 是 Ollama 的 Client 实例,其 chat 方法会向 {base_url}/api/chat 发送 HTTP POST 请求。
  • 请求头部包含 Authorization: Bearer {api_key}(如果已通过环境变量或 client_kwargs 设置)。
  • 请求体为 JSON 格式的 chat_params

2.5 处理响应

  • 如果 stream=True,客户端会逐步返回流式响应块;否则返回完整的响应对象。
  • LangChain 将响应块聚合为 AIMessage,最终返回给调用者。

3. 关键参数映射表

ChatOllama 参数Ollama 客户端参数说明
modelmodel模型标识符
base_urlhost服务地址(不含 /api
temperatureoptions.temperature采样温度
reasoningthink推理模式(True/False/'low'/'medium'/'high')
num_predictoptions.num_predict生成的最大 token 数
stopoptions.stop停止词列表
formatformat输出格式(如 'json')
client_kwargsHTTPX 客户端参数直接传递给底层 HTTP 客户端

4. 示例请求体

最终发送给 Ollama Cloud 的请求体大致如下:

json
{
  "model": "gpt-oss:120b",
  "messages": [
    {
      "role": "user",
      "content": "AI是什么,回答不要超过10个字?"
    }
  ],
  "stream": true,
  "think": true,
  "options": {
    "temperature": 0
  }
}

五、常见错误与解决方案

1. 404 错误

404 错误:path "/api/api/chat" not found

  • 原因base_url 中包含了 /api 路径。
  • 解决:将 base_url 改为 https://ollama.com(末尾可加斜杠,但不能有 /api)。

2. 401 错误

401 错误:unauthorized

  • 原因:API 密钥未正确传递到底层客户端。
  • 解决:检查环境变量 OLLAMA_API_KEY 是否设置正确。或通过 client_kwargs 显式传递 Authorization 头部。

3. 连接错误

连接错误:Failed to connect to Ollama

  • 原因:网络问题或 Ollama Cloud 服务不可用。
  • 解决:检查网络连接,确认 base_url 可访问。