当前位置:首页 > 文章列表 > 科技周边 > 人工智能 > Ollama API 怎样流式读取模型响应

Ollama API 怎样流式读取模型响应

来源:17golang原创 2026-11-03 07:22:55 0浏览 收藏

读取 Ollama 原生 API 的流式响应时,应把响应体当作 application/x-ndjson:按换行读取一条 JSON,解析其中的文本增量并及时显示,直到收到 done: true。不要对整个响应体只调用一次 JSON 解析,也不要把一次网络读取等同于一条消息。HTTP 200 后仍可能出现带 error 的错误记录。

官方地址:https://docs.ollama.com/api/streaming

保护增量文本与完成状态

对一个本地写作助手,真正要保留的是两类资产:已经显示的文本,以及这段文本是否完整结束的状态。它们不能合成一个“有文字就成功”的布尔值。请求中途断开时,界面可能已有几句话,但这几句话只能标记为未完成草稿。

本文只使用原生 /api/generate,文本字段为 response,请求中显式设置 stream: true。/api/chat 的字段结构不同,不能直接照搬;OpenAI 兼容接口的事件格式也不在本文范围内。

本地请求地址采用 http://127.0.0.1:11434/api/generate。运行示例前应已启动 Ollama,并准备一个本机可用的模型,模型名通过环境变量 OLLAMA_MODEL 传入。本文不指定下载模型,也不假设读者已经拥有某个标签。

识别分帧和输出渲染风险

网络层读取的块大小由传输和客户端缓冲决定:一块可能包含多个 JSON 记录,也可能只含半条记录。NDJSON 的记录边界是换行,JSON 字符串里的换行会按 JSON 规则转义。因此本例使用逐行读取,把完整的一行交给解析器。

UTF-8 也可能在任意网络块中被拆开;直接对每个随意分块调用独立的解码操作,可能误报编码错误。逐行累积字节后再严格解码,避免把传输分块误认为字符边界。增量文字不等于一个词或一个 token,不应按照“每条记录恰好一个 token”计算进度。

HTTP响应字节与NDJSON记录及文本字段的静态关系
图1:静态数据结构说明图。字节缓冲、换行记录和 JSON 字段属于不同层级,连线不表示实测网络分包。

若把文字放进网页,应通过文本节点展示,或经受控的 Markdown 渲染与清理流程;不要把原始模型文本直接写成可执行 HTML。以下命令行示例只是把文本写到 stdout,并不提供网页渲染器。

区分请求失败与半途失败

情况读取逻辑业务状态
HTTP 非成功状态处理 HTTP 异常请求失败
流中含 error 字段停止当前读取并关闭响应生成失败,已有文本保留为未完成
EOF 前未见 done=true报告缺少完成记录中断,不能算成功
收到 done=true处理本条文本后结束流式生成结束,另看 done_reason

官方错误文档说明,流开始后发生错误不会再改变原先的 HTTP 状态码。因此只检查 HTTP 200、随后忽略 error 对象,会漏掉真正的半途失败。完成记录也不一定代表回答符合业务要求:例如生成受到长度限制时,仍应结合结束原因提示用户。

用逐行解析和完成标记控制响应

下面的 Python 示例只依赖标准库。请求、读取和结束处理都在同一个上下文中,异常、用户中断和正常结束均会退出 with 块并关闭响应。它限制单行字节数和累计文本字符数,避免客户端无界缓冲;限制数值是示例的业务策略,不是 Ollama 协议上限。

import json
import os
import socket
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

MAX_LINE_BYTES = 1024 * 1024
MAX_TEXT_CHARS = 100_000

def consume(model, prompt):
    request = Request(
        "http://127.0.0.1:11434/api/generate",
        data=json.dumps({
            "model": model,
            "prompt": prompt,
            "stream": True,
        }, ensure_ascii=False).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    total_chars = 0
    completed = False
    reason = None

    # timeout 约束阻塞操作,不是整段生成的总时长预算
    with urlopen(request, timeout=60) as response:
        while True:
            # 限长读取;超长记录直接报错,不继续拼接成无限大对象
            raw = response.readline(MAX_LINE_BYTES + 1)
            if not raw:
                break
            if len(raw) > MAX_LINE_BYTES:
                raise RuntimeError("单条记录超过客户端上限")
            if not raw.strip():
                continue

            part = json.loads(raw.decode("utf-8"))
            if not isinstance(part, dict):
                raise RuntimeError("流记录必须是 JSON 对象")
            if "error" in part:
                # 对外只报固定说明,避免把服务端原文写入公共日志
                raise RuntimeError("服务端返回流内错误,已输出文本不完整")

            text = part.get("response", "")
            if not isinstance(text, str):
                raise RuntimeError("response 字段必须是字符串")
            total_chars += len(text)
            if total_chars > MAX_TEXT_CHARS:
                raise RuntimeError("累计文本超过客户端上限")

            # 先输出本条文本,避免丢掉完成记录可能携带的末尾文字
            sys.stdout.write(text)
            sys.stdout.flush()
            if part.get("done") is True:
                completed = True
                reason = part.get("done_reason")
                break

    if not completed:
        raise RuntimeError("连接结束但没有完成记录,答案不完整")
    return reason

if __name__ == "__main__":
    try:
        # 使用本机已有模型;不在源码里保存密钥或私有服务地址
        selected_model = os.environ["OLLAMA_MODEL"]
        end_reason = consume(
            selected_model,
            "为海边图书角写三条简短的阅读活动建议,不编造日期和价格。",
        )
        print("\n生成结束,原因:" + str(end_reason), file=sys.stderr)
    except HTTPError as exc:
        print("\n请求失败,HTTP 状态:" + str(exc.code), file=sys.stderr)
        sys.exit(1)
    except (URLError, TimeoutError, socket.timeout, OSError,
            ValueError, RuntimeError, KeyError):
        # stdout 中可能已存在部分文字;错误不能将其标成完整回答
        print("\n读取失败,已显示内容应视为未完成草稿。", file=sys.stderr)
        sys.exit(1)
    except KeyboardInterrupt:
        print("\n用户取消,保留已显示草稿。", file=sys.stderr)
        sys.exit(130)

将代码保存为 stream_answer.py,在环境中设置已有模型的名称后运行。stdout 输出文本,stderr 输出结束或错误状态;代码没有在本文中实际执行,也没有附上虚构模型回答。

# OLLAMA_MODEL 应由当前环境设置为本机已有的模型名
python3 stream_answer.py

若所选模型单独返回 thinking 字段,本例不会把它混入答案,只输出 response。把思考内容展示成另一种产品状态,需要另行处理,不应把它当作正文增量拼接。连接关闭也不代表模型一定立即卸载,模型驻留配置与客户端响应关闭是不同职责。

记录结束原因与异常状态

应用至少应区分生成中、已结束、失败和用户取消。可以记录业务请求编号、收到的记录数、累计字数、是否收到完成记录以及结束原因;不必为了排障把完整提示词和生成文本写进公共日志。网页断开或用户取消时,转发服务也应结束相应的上游读取。

完成标记与响应关闭及业务状态的静态依赖
图2:静态依赖说明图。完成记录、流内错误和资源关闭是不同判断维度;不是运行结果或时序流程。

不要把 timeout=60 解读为整个回答最多生成 60 秒:本例是阻塞式标准库客户端,网络操作超时和总时长预算不是一回事。需要严格总时长与即时取消的服务,应采用支持取消和明确 deadline 的客户端设计。

同样,不要在已经显示部分回答后无提示地重试并继续追加。重新生成可能给出不同开头,直接拼接会制造重复或语义断裂。重试应作为新的生成尝试,明确清空、替换或保留旧草稿。

用合成片段判断读取逻辑

下面是人为构造的协议片段,用来解释逻辑,不是真实模型输出。第一条提供正文增量,第二条完成并携带末尾文字,因此阅读器应输出“先列提纲。”并记录结束原因。

{"response":"先列","done":false}
{"response":"提纲。","done":true,"done_reason":"stop"}

若第二条换成 {"error":"example failure"},则不能宣称第一条文字已经构成完整答案;若第二条完全缺失,EOF 也不能替代 done。严格 JSON 记录不支持注释,因此片段中的字段说明放在正文里。

为什么还是最后一次性显示?除客户端有没有 flush,还需考虑服务端、反向代理或前端读取层是否缓冲;curl 的显示选项也不能替代应用中的逐条解析。

能不能直接读取 data: 开头的行?本文原生接口使用 NDJSON,没有 SSE 的 data: 前缀。不要混用不同接口的解析器。

stream=false 怎么处理?它返回一个完整 JSON 对象,适合不需要增量显示的场景;处理方式应与流模式明确分开。

官方生成接口:https://docs.ollama.com/api/generate;流内错误规则:https://docs.ollama.com/api/errors。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
vLLM OpenAI 兼容服务怎样限制并发请求vLLM OpenAI 兼容服务怎样限制并发请求
上一篇
vLLM OpenAI 兼容服务怎样限制并发请求
AI 代理怎样记录工具调用链路和失败原因
下一篇
AI 代理怎样记录工具调用链路和失败原因
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    855次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    859次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    938次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    914次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    744次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码