做一个能逐字回答的聊天界面,与做一个能查询数据、调用业务接口的 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真实编号规则必须来自业务,不能照搬这里的正则。检查通过后,还要根据登录用户查询授权范围。模型提供的“用户编号”不能覆盖后端已经确认的身份。
对于失败,建议返回明确类别:参数错误、无权限、对象不存在、上游超时。不要把所有情况都包装成“查询为空”,否则模型可能把技术故障解释成业务事实。
界面流畅和执行可靠分别验收#
流式展示建议区分“正在生成”“工具查询中”“已获得结果”。工具执行前出现的文字只是中间输出,不能据此宣告订单已更新或部署已完成。
验收时可以准备几组主动构造的场景:普通问答不调用工具;一次响应包含两个查询;参数格式错误;工具超时;调用相同工具多轮仍未收敛;用户在半途取消。每个场景都检查最终消息记录和工具执行次数。
重试尤其要谨慎。查询失败可以按策略重试,写入请求若已经发出但未收到响应,状态可能不确定。此时应查询原请求的执行结果,而不是让模型换一种措辞再执行一次。
另一个值得保留的指标是“有效工具调用率”:参数可用且符合权限的调用,占全部请求执行的调用多少。只看首字延迟,会让系统看起来很快,却掩盖反复调用失败的问题。
并发也应该由应用决定。同一轮里的两个只读查询可以在满足权限和限流要求后并行;先创建对象再读取其详情则存在先后关系。不要因为响应里出现了多个工具,就默认它们可以同时执行。取消会话后,还应阻止尚未开始的调用,并记录已经执行的动作。
接入流式工具调用时,可以先把消息日志、函数白名单和停止条件写清楚。模型与界面之间的链路越明确,出现异常时就越容易判断问题出在生成、解析,还是业务执行。
参考来源#
- Ollama 团队:Streaming responses with tool calling,2025-05-28。
- Ollama 官方文档:Streaming。
- Ollama 官方文档:Tool calling。
- 配图:Ollama 团队,图片出处。
