API 集成:接到自己的程序里

命令行再顺手,也不能让用户帮你敲

前几章里所有和模型打交道的方式都是 ollama run,终端里一问一答,调试的时候无比顺手。但你的知识库最终是给别人用的:同事要在浏览器里点按钮提问,客户要在小程序里等回复,老板想在群里艾特一个机器人。这些场景里没人会帮你敲命令行,也没有人愿意盯着黑窗口等结果。程序和模型之间缺的这座桥,就是 HTTP API。

Ollama 的好处是它把 REST 接口当作一等公民,不是某个付费云服务才有的高级功能。服务一启动,本机 11434 端口就敞开了完整能力:拉模型、列模型、生成回答、多轮对话,命令行能干的 API 全都能干,你甚至可以完全不用 ollama 命令、纯靠 HTTP 把一台机器管起来。市面上那些图形界面工具、知识库平台、编程助手,底层调的也都是这一套接口,你学了就是和它们站在同一层。

市面上那些花里胡哨的 AI 应用,拆开看无非都是这套调用加了一层界面。理解了 API,你用的那些平台就不再是黑盒:出问题知道往哪查,想定制功能也知道从哪里下手。这也是这本书把 API 放在偏后位置的原因,它是前面所有配置和模型选择的最终出口,配置得再好,接不进程序都是自娱自乐。

这一章假设你已经装好服务、配好了模型,现在要把它们接进自己的代码。内容面向写代码的人:两个核心接口的请求长什么样、Python 怎么接、流式输出怎么处理、产品级的封装要注意什么,全都给真实可跑的例子,你照着抄就能动起来。

两个核心接口:generate 与 chat

Ollama 的接口不少,但业务开发九成时间只和两个打交道:/api/generate 和 /api/chat。前者是单项生成,你给一段提示,它给一段回答,没有记忆,适合一次性任务;后者是多轮对话,消息按角色组成数组传进来,适合聊天和知识库问答。选哪个不看心情看场景:拼好提示一次性出结果就用 generate,需要维系会话上下文就用 chat。

这两个看似简单的接口,实际产品里大多数别扭都出在没选对上。一个常见错误是把多轮对话硬塞给 generate:每次自己拼历史进 prompt,拼着拼着格式就乱了,模型接到的是错位的对话片段,回答自然前言不搭后语。反过来也有,明明是一次性抽取任务,非走 chat 绕一圈,消息数组换来换去平白多写代码。判断标准只有一条:上下文需不需要模型帮你维持,需要就 chat,不需要就 generate。

一个具体的选型例子:知识库问答里常见的需求是把用户问题改写成检索关键词,输入输出都是一段文本,没有对话历史,用 generate 一把梭最利索;而客服型的多轮答疑,用户的第二句往往建立在第一句之上,老老实实用 chat 传历史。想清楚这一步,后面的代码结构会顺很多。

先看 generate 的最小调用:

curl http://localhost:11434/api/generate -d "{\"model\":\"llama3.1\",\"prompt\":\"你好,用一句话介绍你自己\",\"stream\":false}"

stream 设成 false 是关键。默认情况下 Ollama 会把一条响应拆成几十个小包往回送,每个包是一段 JSON,那是流式输出用的;调试和初次集成时先关掉,拿到完整 JSON 再说。返回体里 response 字段是答案文本,done 字段标识生成是否结束,eval_count 之类的字段给你 token 计数,做耗时分析很方便。

chat 的写法只是换个字段:

curl http://localhost:11434/api/chat -d "{\"model\":\"llama3.1\",\"messages\":[{\"role\":\"system\",\"content\":\"你是公司知识助手,只依据资料回答\"},{\"role\":\"user\",\"content\":\"新人第一年有几天年假\"}],\"stream\":false}"

messages 数组里每条消息带 role 和 content,角色有三种:system 放系统提示,user 放用户提问,assistant 放模型此前的回答。把历史消息一条条追加进去,下一轮模型就知道上文。消息顺序很重要:system 消息一般放在最前面,接着是一轮轮的 user 和 assistant 交替,模型看到的拼接结果就是这个顺序本身。想要中途调整系统提示也完全允许,比如不同部门的用户进来时换不同的 system,模型立刻就切到对应角色,这是 Modelfile 做不到的灵活性。

注意 Ollama 本身不帮你存历史,每次请求都要把完整对话传过去,这一点和云端对话 API 不同。会话状态存哪、存多久,是你的应用层的事。

两个接口的取向差异用一张表说清:

维度 /api/generate /api/chat
输入结构 单个 prompt 字段 messages 角色数组
上下文 调用方自己拼接 按对话结构依次传入
典型场景 补全、改写、信息抽取 聊天、检索增强问答
系统提示位置 拼在 prompt 开头 单独一条 system 消息
采样参数 options 对象里设 同样支持 options

options 字段两个接口通用,temperature、num_ctx、num_predict 这些 ol-0401 讲过的参数都能通过 JSON 传,不必为每个应用单独建 Modelfile。临时调参走 options,稳定配置走 Modelfile,这是清晰的分工。

顺带记几个管理类接口:/api/tags 列出本地模型,/api/pull 拉新模型,/api/show 看模型详情包括系统提示,/api/delete 删模型。做一个简单的模型管理面板这四个够用。需要说明的是,Ollama 目前没有官方的普罗米修斯式指标端点,想做监控统计得在应用层自己计数,或者到日志里捞数据。应用层计数最省事的办法是封一层客户端,每次调用顺手记下耗时、token 数和状态码,攒几天就是你自己的一手性能数据。

options 里还有个实用字段 format,可以直接指定输出为 JSON。把它设成 json 之后模型会尽量返回合法 JSON,对做信息抽取的应用非常友好。更稳妥的做法是在系统提示里放一段示例 JSON 让它照着填,双管齐下基本能告别解析失败。

用 Python 接进来

curl 验证通了,换到真实代码。Python 是 AI 应用最常用的语言,接法有两种。

第一种是 requests 直接调 HTTP,好处是零黑盒、完全可控,出问题就是 HTTP 那点事:

import requests

OLLAMA_URL = "http://localhost:11434"

def chat(messages, model="llama3.1"):
    resp = requests.post(
        f"{OLLAMA_URL}/api/chat",
        json={"model": model, "messages": messages, "stream": False},
        timeout=300,
    )
    resp.raise_for_status()
    return resp.json()["message"]["content"]

if __name__ == "__main__":
    history = [
        {"role": "system", "content": "你是公司知识助手,只依据资料回答,不知道就说不知道"},
        {"role": "user", "content": "新人第一年有几天年假?"},
    ]
    print(chat(history))

timeout=300 这行不能省。模型生成慢的时候几秒到几分钟都有可能,而 requests 默认不设超时,请求一旦挂死能把整个服务的线程池拖垮。三百秒只是个起步值,内网调试可以宽松,线上按压测结果收紧,超时阈值怎么定后面常见问题里会聊。

第二种是用官方 ollama 库,封装更顺手:

from ollama import Client

client = Client(host="http://localhost:11434")

response = client.chat(
    model="llama3.1",
    messages=[
        {"role": "system", "content": "你是公司知识助手,只依据资料回答"},
        {"role": "user", "content": "新人第一年有几天年假?"},
    ],
)
print(response["message"]["content"])

官方库安装一行命令:pip install ollama。两种接法选哪个看团队:requests 零依赖、人人都会排查;官方库省去样板代码,流式解析也内置好了。内部工具和快速验证我倾向官方库,对链路掌控欲强的项目用 requests,抓个包就知道每一步在干什么,不需要去读第三方库的实现。

还有些细节两个库表现不同,值得心里有数。官方库在连接失败时抛出统一的连接异常,而 requests 的异常类型分得更细,连接错误、超时、状态码错误各是一个类,写 except 时更容易区分。再比如返回结构,官方库把 JSON 解包成属性对象,写起来像访问字段,requests 拿到的是原始字典。这些差异不影响功能,但决定了你封装层写出来的样子,团队统一用一个就行,别混着用。

无论哪种接法,调用地址都要做成配置项而不是硬编码。开发机上是 localhost,部署到服务器上变成内网 IP,哪天上集群又是负载地址,写死在代码里的后果就是每换一次环境改一次代码、提交一次、发一次版。配置项之外再加一个健康检查函数,启动时调一次 /api/tags,连不通就直接报错退出,比用户点进来才发现服务挂了要体面得多。

还有一点要提前考虑:异步。requests 是同步阻塞的,Web 服务里每个请求占一个工作线程,模型回答慢的时候并发几个请求就把线程占满,新用户排队进不来。用 FastAPI 这类异步框架时,把 requests 换成 httpx 的异步客户端,等待模型的时间就能让给别的请求。这个改动越早做成本越低,等流量来了再改全局调用方式是脱层皮的事。

流式输出:让用户少干等

回答一个复杂问题可能要十几秒,用户盯一个转圈图标会以为程序死了。流式输出把生成过程实时推给前端,首字延迟两三秒,后面的字一个个蹦出来,体验立刻不一样。

Ollama 的流式协议简单:响应体是一串 JSON,每行一个,每个包的 message 字段带着一小段增量文本,最后一个包 done 为 true。用 requests 逐行读的写法:

import requests

resp = requests.post(
    "http://localhost:11434/api/chat",
    json={"model": "llama3.1", "messages": [{"role": "user", "content": "介绍一下年假制度"}]},
    stream=True,
    timeout=300,
)
for line in resp.iter_lines():
    if line:
        print(line.decode("utf-8"), flush=True)

用官方库的话加一个 stream=True 参数就行,迭代器直接吐解码好的文本块,协议细节全被封装掉,生产代码里我更推荐这种写法。拿到增量文本之后往哪送,取决于你的前端:网页用 WebSocket 或者服务器推送事件,小程序用其自带的流式接口,原理都一样,把收到的文本块追加到显示区域就行。

流式之外还有一个首字延迟的隐形来源:冷启动。模型如果不在显存里,调用时 Ollama 要先从硬盘把它加载进来,七B 量化模型从固态硬盘加载大约三五秒,机械硬盘能到十几秒,这段时间用户同样在干等。配合 keep_alive 策略调整常驻时间,或者在前端加一句正在准备资源的提示,都能缓解。压测的时候记得分冷启动和热启动两种情况各测一轮,只报热启动数字是自欺欺人。

工程上有两个坑值得提前知道。第一,流式模式下别用单一的总超时,要改成首字超时加空闲超时:首字超过三十秒才算失败,一旦有字流出就放宽限制,只要还在持续吐字就让它继续跑。第二,接到 Web 服务时记得关闭服务器缓冲,不然 Nginx 之类的网关会把小包攒成一坨才往下发,用户那里还是一卡一卡的,流式等于白做。

流式输出还有个常见的疑问:中途想打断怎么办。用户点了停止按钮,前端直接断开连接即可,Ollama 检测到客户端离开会自动终止本次生成,显存和算力立刻释放,不会傻乎乎把剩下的几百个 token 算完。这个行为对体验很重要,意味着用户不用为后悔的提问买单。注意断开后这一次的生成计数不会进入你的统计,做用量报表时心里要有数。

最后讲一个常被忽略的参数 keep_alive。模型加载进显存后默认常驻五分钟,五分钟内没请求就自动卸载。调用时传 keep_alive 可以覆盖:填 0 表示用完即卸,填 -1 表示永不卸载。知识库应用白天高频访问、夜里没人问,让它夜里自动卸载是最省资源的;反之白天高峰期频繁冷启动会多出十几秒的首字延迟,那时就该常驻。

从调用到产品:你需要一层封装

API 本身调通只是第一步。稍微上点规模的应用都建议在 API 之上封一层薄薄的客户端,统一做三件事:超时与重试、错误转换、日志记录。重试只做幂等的读类请求,对话生成本身不幂等,瞎重试容易给用户产出重复回答。错误转换是把连接失败、模型不存在、上下文超长这些底层错误统一翻译成你应用自己的异常码,前端不用关心细节。日志记录则是所有排查工作的前提,谁也不愿意出事时两眼一抹黑。

吹毛求疵一点的话,错误处理也值得单独分类。requests 抛的连接错误、Ollama 返回的 404 模型不存在、上下文超长导致的中断,这三种情况业务侧的处理方式完全不同:连接错误可以重试,模型不存在该报警让人工介入,上下文超长则要触发截断逻辑。在客户端里先分好类,上层代码拿到的就是一个明确的业务异常,不用每个调用点都写一遍判断。这种封装层通常不超过一百行,却是代码库里寿命最长的部分之一。

再往上就是架构问题。把检索、提示拼接、模型调用串成流水线之后,API 调用只是其中一环,检索部分在 ol-0304 里有完整方案。封清楚之后还有一个隐性收益:换模型的成本变低了。今天用 llama3.1,明天想换 qwen 试试,只改客户端里的模型名和提示模板,业务代码一行不动。持续评测两个模型哪个更适合自己的语料,是本地部署相对云服务少有的自由,前提是你的调用层没和某个模型焊死。

等你需要支撑更多用户,记住 Ollama 单实例的吞吐是有上限的,请求在内部排队时延迟会翻倍,扩容前先做体检、拿到数据再动手,别盲目堆实例。

常见问题

为什么我的 curl 返回 404,命令行却能跑

十有八九是路径写错了,正确路径是 /api/chat 不是 /api/v1/chat,Ollama 的接口没有版本前缀,这一点和其他框架的套路不一样,第一次接的人栽得最多。其次是 HTTP 方法,这几个接口都要 POST,写成 GET 同样 404。最后检查模型名,ollama list 里叫什么就传什么,大小写和冒号后的标签都得一模一样。排查时先打印响应体再对照 ol-0403 的报错表,别盯着状态码瞎猜。

默认的 11434 端口被占了怎么办

用环境变量 OLLAMA_HOST 把服务挪到别的端口,重启服务后客户端请求地址同步改掉即可。如果是 Docker 容器里要访问宿主机的 Ollama,记得宿主机上要把监听地址改成 0.0.0.0,否则容器怎么都连不上。

多轮对话的历史存在哪里,Ollama 不会自己记吗

不会,Ollama 是无状态的,每次请求你传什么消息它看什么。历史存在你的应用里:数据库、缓存、会话存储都行。好处是你完全掌控上下文截断策略,坏处是得自己写。历史塞太多会超 num_ctx 上限,超出后早期消息会被悄悄截掉,做长对话产品时务必加一层历史长度检查,别让模型在你不知道的时候失忆。

中文回答里有乱码怎么办

先确认请求头和编码统一是 UTF-8,Python 这边一般不会有问题,curl 在 Windows 的 CMD 里最容易乱码,建议在 Git Bash 或 PowerShell 里测。另一种情况是模型本身的中文能力弱,小模型回复里混拼音和符号很常见,换个中文友好的模型比折腾编码有效得多。

支持 HTTPS 和鉴权吗

Ollama 本身不带认证,也不直接出 HTTPS,它假设你跑在可信内网里。要对外就得在前面加一层 Nginx,HTTPS 和 Basic 认证都由 Nginx 处理,Ollama 只监听内网地址。千万别把 11434 直接映射到公网,等于把显卡算力免费送给全世界。