本地跑 Ollama、线上调 DeepSeek,Python 脚本写起来三行就能出结果,但一放进项目就出问题:请求偶尔卡死、批量跑被限流、流式断在半路、出错只有一句异常看不出是哪层。这篇不重讲协议兼容和客户端配置,只给工程落地的动作清单——怎么设超时、怎么控并发、怎么降级到本地、日志记哪几个字段、上线前怎么自测。C# 侧的对应写法也会顺带对照。
两种调用对象,先统一成一个客户端形状
很多人写脚本时会下意识写两套代码:一套发给本地 Ollama,一套发给云端 DeepSeek。跑起来才发现,除开地址、鉴权和一个模型名字,剩下的请求体、返回解析、流式处理几乎一模一样。把这层相同的东西抽出来,后面换模型、换服务商就不用改业务逻辑。
- base_url
- 请求发到哪台机器,本地一般是本机端口,云端是厂商给的域名
- 鉴权头
- 本地通常不带 key,云端走 Authorization: Bearer,这一处必须分开处理
- 模型名
- 决定用哪个模型,别写死在函数里,做成配置项方便本地和线上互换
抽的时候只留三个入参最省心:messages 是对话内容,模型档位决定用哪个模型,是否流式决定要不要边收边处理。其余像超时、重试次数这种,能给默认值就给默认值,别一开始就堆成十个参数。参数一多,调用方每次都要想一遍填什么,封装的意义就没了。
- 先写一个统一签名定义一个 chat(messages, tier, stream) 这样同名同参的方法,本地和云端各写一个实现,调用方只认这一个名字。
- 鉴权分开处理本地实现里不带鉴权头;云端实现里从环境变量读 key,拼成 Bearer 请求头。两个实现共用同一个请求体构造函数。
- 模型名做成配置把模型名放到配置里,按档位映射到具体名字。本地和线上各有一份配置,切换时只改配置不改代码。
- 验证一次先跑一轮非流式,确认返回结构一致;再把 stream 打开跑一次,确认两边的增量字段能被同一段解析代码吃掉。
| 差异项 | 本地 | 云端 |
|---|---|---|
| base_url | 本机地址,按实际端口填 | 厂商域名,到站内接口文档核对 |
| 鉴权头 | 一般不带 key | Bearer 加环境变量里的 key |
| 模型名来源 | 配置项 A | 配置项 B |
| 返回结构 | 基本同形 | 基本同形 |
有两个地方容易踩坑。一是把 key 写死在脚本里,换个 key 就得翻一堆文件,正确做法是走环境变量,代码里只读不写。二是模型名和档位这种会变的数字硬编码在函数体里,等本地换成线上还得进函数改。这类会随时间和套餐变化的口径,到 /models 核对当时的口径再填,别拿旧值当常量。
超时与流式:分开设,别一刀切
很多人调 Ollama 的 Python 接口时只设一个 timeout,结果要么长回答被自己掐断,要么网络卡住时干等很久。正确做法是把连接超时和读取超时分开放:连接超时管的是「多久连不上就放弃」,读取超时管的是「连上之后多久没动静算超时」。两个值关心的对象不一样,混在一起设必然顾此失彼。
| 超时类型 | 它管什么 | 怎么取值 |
|---|---|---|
| 连接超时 | 从发起到建立连接的等待 | 本地或内网可以设得比较短,公网按最差网络留余量 |
| 读取超时(非流式) | 从发出到收到完整响应的总等待 | 按你预期的最长回答时长再留一点缓冲 |
| 读取超时(流式) | 相邻两次数据之间的最长停顿 | 按「最长停顿」算,不是按整段回答的总时长 |
| 重试等待 | 失败后隔多久再试 | 别设成固定值,用递增;并给整体请求设一个上限 |
流式最容易踩的坑就是把读取超时按「总时长」去设。假设一个长回答要吐很久,但你设的读取超时比整段时长还短,客户端会在回答说到一半时自己断开,日志里看起来像服务端挂了,其实是自己掐的。流式读取超时应该盯的是「两次数据块之间停了多久」,只要模型还在持续吐字,就不该超时。
- 连接超时和读取超时用两个独立参数,别共用一个变量;
- 流式的读取超时按最长停顿来估,别按整段回答总时长;
- 非流式按总时长算读取超时,两者不要互相套用;
- 流式和非流式写成两条独立返回路径,上层按场景选,别在一个函数里用 if 混着返回;
- 给每次请求都打开始、首字、结束三个时间戳,写进日志,后面排查全靠它。
- 先定阈值根据你调的是本地内网还是公网云端,分别定连接超时和两种读取超时;内网本地延迟稳定,读取超时可以放宽,公网按最差网络设,宁可早失败。
- 打三个时间戳请求发出时记开始,收到第一个数据块记首字,全部结束记结束。三个值落进日志,别只打总耗时。
- 按时间戳判断瓶颈首字晚说明模型排队或推理慢,首字正常但中间停顿久说明网络抖或服务端有停顿,结束晚但首字和停顿都正常,多半是你自己的解析或落库慢。
- 流式接生产先做容错断流、重连、首字延迟这些处理思路,参考站内流式输出相关的排查文章,别自己从头造一遍;先把已有方案对照一遍再动手。
判断什么时候可以放宽:内网本地 Ollama,延迟稳定、没有跨公网抖动,读取超时可以设得宽松一些,重点防的是服务真挂了。什么时候必须收紧:跨公网调云端服务,网络路径不可控,就按最差情况设,宁可早失败重试,也不要一个请求挂很久占着连接。这条不适用于你已经做了完善重试和熔断的场景,那种情况下超时可以设得保守一些,靠重试层兜底。
并发、重试与本地降级
批量跑任务时最容易犯的错是把线程开满,几百个请求同时打出去,结果主通道直接限流,重试又把额度烧光。正确做法是用一个固定大小的并发池,把并发数写成配置项,跑之前先小批量试一遍再放大。
| 错误类型 | 是否值得重试 | 处理动作 |
|---|---|---|
| 超时、连接断开 | 可以 | 退避后重试 |
| 服务端 5xx | 可以 | 先降并发,再加退避 |
| 鉴权失败 | 不要 | 检查 key 和权限,重试白费 |
| 参数错误 | 不要 | 改参数,重试多少次都一样 |
| 限流 429 | 不要立刻重试 | 降并发,再按退避逐步恢复 |
- 把并发数做成配置在代码里留一个 concurrency 变量,默认值保守一点,别写死在逻辑里;跑批量任务前先改成小值验证通路。
- 区分可恢复与不可恢复错误只有超时、连接断开、服务端 5xx 进重试分支;鉴权和参数错误直接抛出来,进重试队列只会白烧额度。
- 先降并发再加退避遇到限流或大量 5xx,先把并发砍一半,再给重试加指数退避和随机抖动;不要一上来就疯狂重试。
- 主通道失败若干次后切本地云端主通道连续失败到设定阈值,切到本地 Ollama 兜底,并把降级次数单独计数,方便事后判断主通道健康度。
- 按排查顺序定位先确认单次请求能不能通,再看并发下是否触发限流,最后查本地机器显存是否不够导致请求排队。
- 本地兜底模型能力弱是预期内的,别拿它跑需要长上下文或高精度推理的任务
- 降级计数要单独存,和正常成功数分开看,否则主通道烂了也发现不了
- 本地服务能扛多少并发,去站内 /compute 一类页面核对当时的数字,别信记忆里的旧值
- 重试上限要写死,不能无限循环,否则一个坏请求能卡住整个批次
- 批量任务最好记录每条请求走了主通道还是兜底通道,方便回查
日志字段与上线前自测
接线阶段最怕的不是报错,是「跑通了但说不清」。所以日志字段先定死,后面出问题才有东西可查。一条请求至少记这几项:发起时间、调用目标是本地还是云端、模型名、是否流式、耗时(连接、首字、整体三段)、状态码、错误类型、token 或字符数估算。缺哪一项,事后都复盘不出来。
| 字段 | 记什么 | 缺了会怎样 |
|---|---|---|
| 时间 | 请求发起与结束的时间点 | 分不清是并发高峰还是单次抖动 |
| 调用目标 | 本地服务还是云端接口 | 换环境后无法对比,排查方向直接错 |
| 模型名 | 实际请求的模型标识 | 换模型后效果变差,找不到是哪次改的 |
| 是否流式 | 流式或一次性返回 | 首字延迟和总耗时的关系解释不了 |
| 耗时三段 | 连接、首字、整体 | 只能看到总时长,定位不到卡在哪一步 |
| 状态码与错误类型 | HTTP 状态码加异常分类 | 超时和鉴权失败混在一起,修错地方 |
| 用量估算 | token 或字符数 | 成本与限流问题算不清账 |
耗时拆三段是有原因的:连接慢多半是网络或服务没起来,首字慢多半是模型在排队或加载,整体慢才是生成本身的问题。只记总耗时,这三类问题看起来一模一样。用量估算不要求精确,按字符数粗估也够用,关键是能对比同一批任务的量级变化。
- 正常回答发一条最普通的请求,确认能拿到完整回复,日志里七个字段都有值。
- 超长输入塞一段明显偏长的文本,看是否触发截断或报错,同时确认耗时字段没有变成空值。
- 空输入传空字符串或空消息列表,确认程序给出明确错误而不是静默挂住。
- 故意写错 key用错误的密钥调一次,确认错误类型被正确归类,而不是混进通用异常里。
- 断开网络拔网线或断掉本地服务,确认超时被触发,且日志能区分连接失败和读取超时。
这五条用例固定下来跑一遍,基本能覆盖大部分接线问题。它们的价值不在于通过,而在于每条失败时日志都能告诉你失败在哪一层。如果某条用例只报了个笼统的异常,先去补字段,再回来重跑。
用 C# 写的话,思路和 Python 一样,只是语法不同。HttpClient 要复用同一个实例,别每次请求都 new,否则连接池耗尽会出现莫名其妙的失败;取消令牌要传下去,用它来控制超时,而不是自己写计时器;流式读取用 IAsyncEnumerable 逐段消费,配合取消令牌一起用。判断标准很简单:只在自己机器上试玩,日志可以先简;一旦给别人用或者跑批,缺字段的日志等于没有。
下一步把日志字段和自测用例落成一份固定清单,以后每次改动都照着跑一遍,比出问题再回头翻代码省事得多。→常见问题
Python 调 Ollama 需要装官方 SDK 吗?
不是必须。直接用 requests 或 httpx 打本地 HTTP 端点最省事,少一层依赖也少一个版本坑。官方库适合你要用到它封装的模型管理、拉取等功能时再上。判断标准很简单:只做对话调用就用通用 HTTP 客户端,要管模型生命周期再引库。
本地跑通了,换成云端接口为什么报鉴权错?
最常见的是本地不带 key、切云端时忘了加鉴权头,或者 key 读的环境变量名对不上。先把请求头和 base_url 打印出来对一遍,再确认 key 有没有多余空格。还不行就按状态码分头查,401 和 403 的原因不是一回事,站内有专门文章讲这个区别。
批量调用时怎么避免把额度烧光或者被限流?
先算清单次大概消耗多少 token,乘上批量条数,再决定要不要分批跑。执行时用固定并发池加失败重投,并且给整批设一个总预算上限,超了就停。具体单价和限流档位随时会变,到 /official-api 看你所用厂商当时的数字,别拿旧笔记估算。