你在 C# 项目里想用本机跑的 Ollama,照着网上例子写了个 HttpClient,结果要么卡住不返回,要么流式输出断在半路,要么换了端口就 404。这篇不讲协议原理,只按一个能跑起来的顺序,把请求组装、返回解析、超时与并发设置、日志该记哪些字段捋一遍,顺带告诉你哪些做法在什么情况下不适用。
你在 C# 项目里想接本机跑的 Ollama,照着网上例子写了个 HttpClient,结果要么一直卡着不返回,要么流式输出断在半路,要么换个端口就 404。问题通常不在协议本身,而在几个很具体的动作上:端点选错、请求体字段写错、流式按整段读、超时按普通接口设。
这篇不讲原理,只按一个能跑起来的顺序,把请求组装、返回解析、超时并发、日志与降级捋一遍。每一步都会说清什么情况下不适用,比如你其实是把 Ollama 挂在另一台机器上、或者已经有反代在中间,那有些默认做法就不能照搬。
先把请求组对:端点、方法体与两个易错点
第一件事是分清走哪类入口。原生端点用 /api/chat 和 /api/generate,字段是 model 加 messages 或 prompt;如果你已经按 OpenAI 兼容格式改造过调用层,就走 /v1/chat/completions。这两套字段名不一样,混着拼 body 会得到一个看起来像 400 的报错,但根源其实是字段对不上。
| 项 | 原生端点 | OpenAI 兼容入口 |
|---|---|---|
| 主路径 | /api/chat、/api/generate | /v1/chat/completions |
| 消息字段 | messages 或 prompt | messages,角色命名不同 |
| 流式控制 | body 里的 stream 布尔值 | 同样靠 stream 字段 |
| 返回结构 | 逐行 JSON(流式时) | SSE 风格分块 |
第二件事是别硬编码。base 地址、模型名、超时都放配置里。模型名要和你本机 ollama list 看到的完全一致,大小写和 tag 写错会直接返回找不到模型,这个错在到达你的解析代码之前就抛出来了,所以别把它的异常当成 JSON 解析失败去查。
- POST 请求,Content-Type 用 application/json,不要用表单编码
- body 里 stream 显式写 true 或 false,不要指望服务端替你判断
- 端口被占用换端口时只改配置,不改代码里的字符串
- 先用最简单的非流式请求跑通,再上流式,排查面小很多
返回怎么解析:一次性、流式、以及半截 JSON 的处理
非流式最简单,整个响应是一个完整 JSON,写一个 DTO 接住就行。字段名建议用属性映射注解显式对上,别依赖默认命名转换。命名风格差异导致的后果是所有字段读出来都是空值,代码不报错,你在业务层看到的是模型好像什么都没回答。
流式就麻烦一些。响应是逐行 JSON,你必须按行切分,每行单独反序列化。常见的错误是一次性 ReadToEnd 再当整体解析,这样首字延迟和内存占用都很难看,流式的意义就没了。另一个高频坑是网络分片:一行可能分两次到达,两行也可能挤在一次读取里。
- 先缓冲再切行把每次读到的片段追加进缓冲区,按换行符切;切不出完整行的残片留在缓冲区,等下一轮拼接。
- 逐行反序列化对每个完整行单独反序列化,不要合并多个 JSON 对象再解析。
- 单行失败跳过某一行解析抛异常时,记下原始文本、跳过这行、继续读后面的内容,不要让整条请求崩掉。
- 识别结束条件流式结束时服务端通常会给一个带结束标记的行,不识别它你就分不清是正常收尾还是断流。
超时、并发和失败重试怎么设才不坑自己
本机模型首次加载会明显慢,超时不能照搬普通 HTTP 接口的经验值。建议连接超时和读取超时分开放:连接超时给短一点,快速暴露服务没起来的问题;读取超时给足余量,整体超时交给业务侧控制。具体给多少和你的机器规格、模型大小有关,可以去 接口测速(/tools/speed-test) 先量一遍本机延迟基线再定数值。
并发不要一上来就压满。本机推理算力和显存有限,同一时刻跑太多请求只会互相抢资源,表现出来的现象是每个请求都变慢,而不是某个请求报错。正确顺序是先按顺序跑通,再按机器能力逐步加并发并观察响应时间变化。
| 失败现象 | 是否适合重试 | 原因 |
|---|---|---|
| 连接被拒绝、端口不通 | 适合 | 服务可能刚起,还没就绪 |
| 请求超时 | 谨慎,有限次退避 | 盲重试会让本地队列越堆越长 |
| 模型返回 500 | 通常不适合 | 多为模型或显存问题,重试解决不了 |
| 跨主机网络抖动 | 按网络策略单独配 | 和本机进程内调用不是一回事 |
- 同一时刻的并发数先按机器能力设一个小上限,观察稳定后再调
- 重试用有限次退避,别用固定间隔死循环
- 跨机器调用和本机调用分开配超时与重试策略
- 服务刚启动的前几秒单独留出就绪等待,别直接算作失败
日志记什么、失败降级怎么做
日志字段先定下来再写代码,不然后面复盘全靠猜。每次调用至少落这几个:请求时间、模型名、是否流式、总耗时、首字耗时、输入输出 token(能拿到就拿)、结束原因、错误原文。流式场景要单独记首字耗时和是否正常收尾,因为断流往往只表现为「没有结束标记」,不记这个字段根本发现不了。
降级顺序建议固定成一条链:本地模型失败先重试一次,仍失败再切到备用模型或备用端点。可用的备选放在配置里,不要写死在代码分支里。具体哪家模型适合当兜底,可以到 模型资料库(/models) 按能力筛选后填进配置。
- 用静默 catch 吞异常是最大的排查成本来源,别这么干
- 本地服务没启动、模型没拉下来、显存不足这三类报错信息完全不同,原样打日志最省时间
- 降级前先确认备用端点真的可用,否则只是把失败往后拖一步
- 日志里带上请求标识,流式多行输出才好串成一次调用
按这个顺序做下来:先固定端点与请求体字段,再处理流式分片,然后配好分层超时和有限并发,最后把日志字段和降级链补上。每一步都能单独验证,出问题时也知道该看哪一层。
下一步本机延迟基线和不同模型的响应差异会直接影响你的超时与并发设置,先量一遍再写死参数,后面改动会少很多。→常见问题
C# 调 Ollama 需要额外装 SDK 吗?
不需要。直接用 .NET 自带的 HttpClient 发 POST 请求就行,额外引入第三方封装包反而多一层版本耦合。自己封装一个小的调用类,把地址、模型名、超时暴露成配置项,通常比装包更容易排查问题;只有当你要接的功能明显超出单个接口调用的范围时,才值得考虑引入现成库。
流式输出在 C# 里一直读不完或者卡住怎么办?
先确认 stream 设成了 true,再检查是不是用 ReadToEnd 之类的方式一次性读完整流——流式必须边读边处理。其次确认读取超时没设太短,模型还在生成时连接是正常的。最后用命令行或别的工具对同一个接口发一次请求,能正常流式返回就说明问题在客户端读取方式上,去 中转站真假在线实测(/tools/verify) 看探针思路也有帮助。
怎么判断慢是模型本身慢还是我的调用代码慢?
把耗时拆成三段看:建立连接的时间、首字返回的时间、全部读完的时间。连接慢基本是网络或服务没就绪,首字慢是模型加载或排队,读完慢是生成长度问题。只记一个总耗时永远分不清。先去 接口测速(/tools/speed-test) 量一个本机基线,再拿你的代码结果去对照。