↓ 跳过正文

Ollama 流式工具调用:从逐字输出到完整执行闭环

正能量5 分钟阅读

解释 Ollama 流式响应与工具调用如何协作,说明增量消息收集、参数校验、结果回传,以及 Agent 循环的停止和异常处理。

Ollama 流式工具调用发布封面,羊驼持盾并注明支持流式工具调用

做一个能逐字回答的聊天界面,与做一个能查询数据、调用业务接口的 AI 助手,中间还隔着一层执行系统。前者关心文字何时出现,后者还必须回答:谁决定执行什么、参数什么时候完整、执行失败后怎样继续。

接入时可以把链路拆成四步:收集完整消息、校验工具调用、执行业务函数,再把结果交回模型。流式展示与业务执行分别处理。

流式解析与工具执行的边界
#

Ollama 可以在返回流式文字的同时识别工具调用。服务端解析器会结合模型模板区分普通内容与工具结构,减少“等全部生成结束后再判断是不是工具”的等待。它解决的是输出解析和展示问题,实际函数仍由接入应用执行。

这对业务开发者的意义很直接:用户可以先看到响应进度,应用随后拿到结构化调用。但“流开始了”与“工具可以执行了”依然是两个时刻。

先把四个角色分清楚
#

可以把一次请求拆成四个角色:

角色主要责任容易混淆的地方
模型根据问题和工具描述生成调用意图输出参数并不等于参数可信
Ollama 服务运行模型并解析响应解析出函数名不等于执行该函数
应用后端校验、授权、执行工具、组织下一轮消息这里才是业务规则的入口
前端展示文字、等待状态和结果不应凭展示文字触发业务动作

例如,模型生成“查询订单状态”的调用,后端仍要确认当前用户是否能访问该订单。自然语言提示可以帮助模型理解流程,不能代替服务端的权限判断。

建议先接入查询类工具,如查询订单或读取公开文档。等消息记录、错误处理和停止条件清楚之后,再考虑会产生写入的操作;写入还需要业务自身的幂等与确认机制。

流式处理有三种“完整”
#

传输完整:先收到一条 JSON
#

Ollama 原生流式接口使用按行分隔的 JSON,即 NDJSON。收到的是连续的 JSON 记录,不是一个不断变长的 JSON 数组。

网络读取的一次回调可能只有半行,也可能包含多行。因此,如果自己实现 HTTP 客户端,应先缓存字节、识别完整行,再解析 JSON。用成熟客户端的行迭代器会更省事。把每次 TCP 读取都交给 JSON 解析器,会在网络正常的情况下也出现“JSON 不完整”。

消息完整:收集这一轮的输出
#

流式处理中需要累积 thinking、content 和 tool_calls,随后把本轮 assistant 消息和工具结果一起放进后续请求。

这里应按具体协议处理。Ollama 原生接口与其他供应商的增量协议并不完全一样:已经是对象的工具参数,不应该再次作为 JSON 字符串拼接;若另一个协议按调用编号发送参数字符串片段,则应先按编号归并,再解析。不要写一个假定所有供应商都相同的“通用拼接器”。

业务完整:得到结果,再让模型回答
#

控制流程可以写成下面的伪代码:

最多执行 6 轮:
  请求模型,逐条读取完整响应记录
  向界面转发可展示文字,同时累积本轮消息
  把完整 assistant 消息追加到会话
  如果没有工具调用,结束

  逐个处理工具调用:
    校验函数名、参数、用户权限与剩余预算
    执行工具,得到成功结果或结构化错误
    以 tool 角色追加结果

超过轮数仍未结束:停止并返回当前进度

“6 轮”是示例应用的保护值,并非 Ollama 的固定限制。实际项目应根据任务长度和成本设置,同时限制一次响应中的工具数量、单工具超时与总耗时。

工具结果怎样放回会话
#

以一个虚构的包裹查询为例,模型提出调用后,会话可以包含下面两条消息。订单号和返回值都是演示数据。

[
  {
    "role": "assistant",
    "content": "",
    "tool_calls": [
      {
        "function": {
          "name": "lookup_parcel",
          "arguments": {"parcel_id": "DEMO-001"}
        }
      }
    ]
  },
  {
    "role": "tool",
    "tool_name": "lookup_parcel",
    "content": "{\"status\":\"in_transit\",\"checked_at\":\"2026-10-20T10:00:00+08:00\"}"
  }
]

这两条消息追加在原有 user 消息之后,而不是替换整段历史。下一轮模型才能知道刚才调用了什么,以及哪个信息来自工具。接入时应对照所锁定的 Ollama 原生接口与客户端版本。

对于同名函数的多次调用,应用内部还应记录各次调用与执行结果的对应关系。不要只保留“最近一个查询结果”,否则多个包裹或多个用户对象可能发生串用。

参数校验要放在执行之前
#

工具入口可以先校验函数名和参数,再进入鉴权与查询。下面的 Python 片段只处理第一步:

import re

def validate_lookup(name, arguments):
    if name != "lookup_parcel":
        raise ValueError("工具未在白名单中")
    if not isinstance(arguments, dict):
        raise ValueError("参数必须是对象")
    if set(arguments) != {"parcel_id"}:
        raise ValueError("参数字段不匹配")
    parcel_id = arguments["parcel_id"]
    if not isinstance(parcel_id, str):
        raise ValueError("包裹编号必须是字符串")
    if not re.fullmatch(r"DEMO-[0-9]{3}", parcel_id):
        raise ValueError("演示包裹编号格式错误")
    return parcel_id

真实编号规则必须来自业务,不能照搬这里的正则。检查通过后,还要根据登录用户查询授权范围。模型提供的“用户编号”不能覆盖后端已经确认的身份。

对于失败,建议返回明确类别:参数错误、无权限、对象不存在、上游超时。不要把所有情况都包装成“查询为空”,否则模型可能把技术故障解释成业务事实。

界面流畅和执行可靠分别验收
#

流式展示建议区分“正在生成”“工具查询中”“已获得结果”。工具执行前出现的文字只是中间输出,不能据此宣告订单已更新或部署已完成。

验收时可以准备几组主动构造的场景:普通问答不调用工具;一次响应包含两个查询;参数格式错误;工具超时;调用相同工具多轮仍未收敛;用户在半途取消。每个场景都检查最终消息记录和工具执行次数。

重试尤其要谨慎。查询失败可以按策略重试,写入请求若已经发出但未收到响应,状态可能不确定。此时应查询原请求的执行结果,而不是让模型换一种措辞再执行一次。

另一个值得保留的指标是“有效工具调用率”:参数可用且符合权限的调用,占全部请求执行的调用多少。只看首字延迟,会让系统看起来很快,却掩盖反复调用失败的问题。

并发也应该由应用决定。同一轮里的两个只读查询可以在满足权限和限流要求后并行;先创建对象再读取其详情则存在先后关系。不要因为响应里出现了多个工具,就默认它们可以同时执行。取消会话后,还应阻止尚未开始的调用,并记录已经执行的动作。

接入流式工具调用时,可以先把消息日志、函数白名单和停止条件写清楚。模型与界面之间的链路越明确,出现异常时就越容易判断问题出在生成、解析,还是业务执行。

参考来源
#