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 安装相关依赖库
pip install -qU langchain-ollama1.2 实例化模型对象
from langchain_ollama import ChatOllama
llm = ChatOllama(
model="llama3.1",
temperature=0,
# other params...
)1.3 调用模型
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 模型,在这里就使用这个模型:
# -*- 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)运行后会得到如下内容:
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 的接口调用看一下:
# -*- 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)可以看到如下输出信息:
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. ChatOllama 的 base_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) 这个类:
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 方法。该验证器在 实例的所有字段赋值完成后自动执行。
@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 会先验证并设置所有字段(如model、base_url、temperature等)。(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_headers 为 None。
3.3 parse_url_with_auth()
在 langchain_ollama/_utils.py 中,parse_url_with_auth 函数负责解析 URL 并提取认证信息。
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_headers 为 None。其中 parsed.path 会被保留并附加到清理后的 URL 中。这意味着如果 base_url 包含路径(如 /api),该路径会保留。
3.4 传递给 Ollama 客户端的 host 参数
在 ChatOllama 的 _set_clients 方法(位于 langchain_ollama/chat_models.py)中,清理后的 URL 被作为 host 参数传递给 Ollama 的 Client 构造函数:
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。:
self._client = client(
base_url=_parse_host(host or os.getenv('OLLAMA_HOST')),
follow_redirects=follow_redirects,
timeout=timeout,
headers=headers,
**kwargs,
)_parse_host() 的核心逻辑如下(简化):
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 将是 /api,strip('/') 后得到 'api',最终基础 URL 变为 https://ollama.com:443/api(注意端口 443 是默认的 HTTPS 端口)。
由于标准 HTTPS 端口是 443,实际请求时浏览器和 HTTP 客户端通常会省略
:443,但底层连接仍使用该端口。
3.6 请求路径拼接
Ollama 客户端在发起请求时,会在基础 URL 后追加固定的端点路径,例如 /api/chat。拼接方式为:
最终请求 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=443,path="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 关键点
- 调用时机:仅在初始化时调用一次。后续的
invoke、stream等操作不再重复解析 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()
下面是函数调用关系逻辑:
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。
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()函数,这个函数定义如下:
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():
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 方法:
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.request 或 self._client.stream。self._client 是 httpx.Client 实例,其 base_url 已在初始化时设置为 "https://ollama.com:443"。当请求路径为 /api/chat 时,httpx 会自动将其拼接到 base_url 后,形成完整的请求 URL:
https://ollama.com:443/api/chat由于 HTTPS 默认端口 443 可以省略,实际等效于 https://ollama.com/api/chat。
5. 小结
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']。
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 客户端。
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. 构造函数参数
llm = ChatOllama(
model="gpt-oss:120b",
base_url="https://ollama.com/", # 使用 Ollama Cloud
temperature=0,
reasoning=True,
# other params...
)2. 参数处理步骤
2.1 初始化 ChatOllama 实例
model、base_url、temperature、reasoning等参数被赋值给实例的对应属性(定义在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。- 将
model、temperature、reasoning等实例属性与调用时传入的额外参数合并,生成options_dict。 - 返回的参数字典包含:
messages: 转换后的消息列表stream: True(默认流式响应)model: 使用的模型名称think: 推理模式(对应reasoning参数)options: 包含temperature等生成选项的字典- 其他可选参数(如
format、keep_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 客户端参数 | 说明 |
|---|---|---|
model | model | 模型标识符 |
base_url | host | 服务地址(不含 /api) |
temperature | options.temperature | 采样温度 |
reasoning | think | 推理模式(True/False/'low'/'medium'/'high') |
num_predict | options.num_predict | 生成的最大 token 数 |
stop | options.stop | 停止词列表 |
format | format | 输出格式(如 'json') |
client_kwargs | HTTPX 客户端参数 | 直接传递给底层 HTTP 客户端 |
4. 示例请求体
最终发送给 Ollama Cloud 的请求体大致如下:
{
"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可访问。