Python Ollama 本地 Agent 入门与订单查询实践

问模型"订单 2001 一共多少钱",它可以给出一句像客服的回答,却不一定知道订单里实际有哪些商品。只要答案依赖业务数据,就需要让程序查询真实记录,再把结果交给模型组织语言。

这个教程面向会写基础 Python 的开发者。我们用 Ollama、SQLite 和一个查询函数,搭起一次工具调用的完整流程:模型识别需求,Python 校验并执行,模型根据返回值回答。示例采用虚构书店订单,不连接真实业务系统,也不需要云端模型账号。

先区分模型请求与程序执行

工具调用(Tool Calling)是模型生成的一份操作请求,例如"调用 query_order_amount,参数为 order_id=2001"。真正连接数据库、执行 SQL、返回结果的是 Python。模型看到的是工具名称、用途和参数结构,而不是函数内部的 SQL。1

这也是入门 Agent 值得先拆开学习的地方:模型负责选择动作,应用负责执行动作,并把结果放回对话。本例只完成一轮"请求工具---执行工具---返回结果",最多发出两次模型请求。它有一个工具,但不等于整个程序只调用一次模型。

先约定可检查的目标:订单 2001 包含两本单价 39.90 元的书和一个 12.50 元的书签,总计 92.30 元。我们同时检查工具有没有选对、订单号有没有取对,以及最终回答有没有改动金额。

准备可运行的本地环境

下面的命令使用 Windows PowerShell。需要事先安装 uv 和 Ollama;安装入口见文末官方资料。本文选择 Python 3.12 作为项目解释器,不要求追随最新 Python 版本。

先下载模型并建立独立项目:

ollama pull qwen3.5:4b

uv init --python 3.12 bookshop-agent

cd bookshop-agent

uv add ollama

qwen3.5:4b 在 Ollama 模型页标注了工具调用能力,但这不保证它每次都能正确选取工具。2 模型下载需要网络;下载完成后,本例使用本地 Ollama 服务推理。磁盘占用和运行内存是不同指标,选模型时还要考虑上下文长度和设备余量。

uv add 管理项目依赖,uv run 在项目环境中运行命令,因此不用手动激活虚拟环境。3 SQLite 属于 Python 标准库。项目里只需自己创建两个文件:init_db.py 和 agent.py,数据库文件会由初始化脚本生成。

用整数金额建立测试数据

先完成与模型无关的数据库部分。金额以"分"保存为整数,避免把二进制浮点数当成精确的货币表示。下面的脚本创建虚构订单,重复运行时保留已有表和记录,不删除文件。

保存为 init_db.py:

import sqlite3

from contextlib import closing

from pathlib import Path

DB = Path(file).with_name("bookshop.db")

with closing(sqlite3.connect(DB)) as conn:

execute("PRAGMA foreign_keys = ON")

executescript("""

CREATE TABLE IF NOT EXISTS orders (

order_id INTEGER PRIMARY KEY

);

CREATE TABLE IF NOT EXISTS items (

item_id INTEGER PRIMARY KEY,

order_id INTEGER NOT NULL REFERENCES orders(order_id),

name TEXT NOT NULL,

quantity INTEGER NOT NULL CHECK (quantity > 0),

price_fen INTEGER NOT NULL CHECK (price_fen >= 0)

);

""")

execute("INSERT OR IGNORE INTO orders VALUES (?)", (2001,))

executemany(

"INSERT OR IGNORE INTO items VALUES (?, ?, ?, ?, ?)",

[(1, 2001, "Python book", 2, 3990),

(2, 2001, "Bookmark", 1, 1250)],

)

commit()

print(f"Ready: {DB}")

运行 uv run python init_db.py,确认同目录出现 bookshop.db。如果自行修改了示例数据,INSERT OR IGNORE 不会覆盖原来的记录;请在新的练习目录初始化,或自行明确更新目标记录。

给模型一项范围明确的能力

查询函数只接受订单号,只返回订单金额。不要给第一个示例开放任意 SQL 或通用命令执行接口,否则学习工具调用的同时,还得处理更大的权限范围。

先把下面这一段保存为 agent.py,后面的两段依次追加到同一个文件中:

import json

import os

import sqlite3

import sys

from contextlib import closing

from pathlib import Path

from ollama import chat

DB = Path(file).with_name("bookshop.db")

MODEL = os.getenv("OLLAMA_MODEL", "qwen3.5:4b")

def query_order_amount(order_id: int) -> str:

"""Return the recorded order amount in CNY.

Args:

order_id: Positive integer order number.

"""

uri = DB.resolve().as_uri() + "?mode=ro"

with closing(sqlite3.connect(uri, uri=True)) as conn:

row = conn.execute("""

SELECT o.order_id,

COALESCE(SUM(i.quantity * i.price_fen), 0)

FROM orders AS o

LEFT JOIN items AS i ON i.order_id = o.order_id

WHERE o.order_id = ?

GROUP BY o.order_id

""", (order_id,)).fetchone()

if row is None:

data = {"error": "ORDER_NOT_FOUND", "order_id": order_id}

else:

total = row1

data = {

"order_id": row0, "currency": "CNY",

"amount": f"{total // 100}.{total % 100:02d}",

}

return json.dumps(data, ensure_ascii=False)

这里有三处值得保留:路径相对于脚本定位;连接使用只读模式,数据库不存在时不会偷偷创建空库;订单号通过 ? 占位符传入,不拼接 SQL。closing 会明确关闭连接,避免把事务上下文误当成连接关闭机制。

LEFT JOIN 用来区分"不存在的订单"和"存在但没有商品的订单"。前者返回错误,后者金额为 0.00。金额返回为字符串,是为了保留两位小数;真实业务还应明确优惠、运费、税费和退款的计价规则。

在应用侧校验工具请求

函数的类型注解会帮助 SDK 生成参数说明,但它不会自动阻止错误类型进入函数。下面把名称白名单、字段检查和整数检查放在执行之前。继续追加到 agent.py:

def execute_tool(call) -> str:

if call.function.name != "query_order_amount":

raise ValueError("Unknown tool")

args = call.function.arguments

if not isinstance(args, dict) or set(args) != {"order_id"}:

raise ValueError("Expected only order_id")

order_id = args"order_id"

if type(order_id) is not int or order_id <= 0:

raise ValueError("order_id must be a positive integer")

print("Tool request:", call.function.name, args)

result = query_order_amount(order_id)

print("Tool result:", result)

return result

这里使用 type(order_id) is int,会拒绝字符串、浮点数和布尔值。Python 中 bool 是 int 的子类,仅用 isinstance(value, int) 会把 True 也当作合法整数。

对这个练习来说,遇到非法请求直接停止就够了。后续需要自动纠错时,可以把结构化校验错误返回给模型,但应设置重试上限。也不要从模型返回的名称中动态查找或执行任意函数。

把执行结果放回对话

最后追加入口函数。第一轮允许模型请求工具,第二轮只要求它根据已取得的结果答复:

def main():

question = " ".join(sys.argv1:) or "订单 2001 一共多少钱?"

messages = [

{"role": "system", "content": (

"Answer in Chinese. Use query_order_amount for order amounts. "

"Do not invent shop facts. Preserve the returned amount and "

"currency. If data is unavailable, explain that limitation."

)},

{"role": "user", "content": question},

]

response = chat(model=MODEL, messages=messages,

tools=query_order_amount)

calls = response.message.tool_calls or \[\]

if len(calls) != 1:

raise RuntimeError("Expected exactly one tool call")

result = execute_tool(calls0)

append(response.message)

append({

"role": "tool", "tool_name": calls0.function.name,

"content": result,

})

final = chat(model=MODEL, messages=messages)

if final.message.tool_calls or not final.message.content.strip():

raise RuntimeError("Expected a final text answer")

print("Agent:", final.message.content)

if name == "main":

main()

Ollama Python SDK 支持把函数直接放入 tools,根据函数签名和文档字符串生成工具描述。1 我们保留第一轮的 assistant 消息,让模型在第二轮看到自己发出的请求,再用 tool 消息提供执行结果。

这个入口刻意规定只能调用一次工具。它适合观察一次完整交互,不是通用多步 Agent。第二轮不再提供工具列表,也没有无限循环;如果以后支持多步任务,应另外限制轮次、超时和累计执行成本。

验收时分别检查数据和模型行为

运行 uv run python agent.py。正常请求应产生下面的工具记录,最后一句为示意,实际模型措辞可能不同:

Tool request: query_order_amount {'order_id': 2001}

Tool result: {"order_id": 2001, "currency": "CNY", "amount": "92.30"}

Agent: 订单 2001 的总金额是 92.30 元人民币。

工具记录正确,只能证明 Python 查到了正确值,不能保证模型一定忠实转述。如果金额不能容许任何偏差,更可靠的做法是由程序根据结构化结果直接生成金额句子,或在输出前核对模型答案。模型可以补充解释,但结算金额应以业务系统为准。

接着运行两类失败场景:

uv run python agent.py "订单 9999 一共多少钱?"

uv run python agent.py "订单 2001 什么时候送到?"

第一条应查得 ORDER_NOT_FOUND,最终回答应说明订单不存在。第二条超出了工具能力:金额数据没有物流字段。模型可能不调用工具而触发入口异常,也可能调用金额工具后解释缺少物流数据;这两种结果都不能当成已经完成配送查询。如果它猜出到货日期,验收应判为失败。

排错时按结果发生的位置检查:

无法连接 Ollama:先确认应用或服务正在运行,再用 ollama list 检查模型是否已下载。

unable to open database file:确认先运行了初始化脚本,数据库与 agent.py 位于同一目录,且进程有读权限。

Expected exactly one tool call:检查问题是否属于金额查询,再查看模型返回的工具请求;系统提示无法代替执行侧检查。

order_id must be a positive integer:参数类型不合格,应修正请求或采用有上限的重试。

工具结果正确但回答错误:优先改用确定性的结果模板,再评估是否需要更换模型。

更换已安装且支持工具调用的模型,可以在当前 PowerShell 会话中设置 $env:OLLAMA_MODEL = "你的模型名称"。比较模型时,记录它是否选对工具和参数,而不仅仅是最终句子是否流畅。

从练习脚本走向真实服务

上线前需要增加用户身份和订单归属校验。只读 SQL 与工具白名单能够缩小操作范围,但不能证明提问者有权查看某个订单;业务权限必须由可信应用上下文决定,不能相信模型自报的用户身份。

如果希望多人使用或长期提供查询,需要把脚本封装成服务,并补上进程管理、请求超时、日志脱敏、鉴权和访问限制。应用与推理服务可以分别部署,模型选择则按内存或显存、并发和可接受延迟评估。

迁移到远程环境时,可以评估 Hostease 或其他服务器资源,并按应用负载与模型推理需求确认配置。本地跑通只是迁移的起点,服务器选型不会替代权限控制和输出验收。

下一步建议先增加第二个只读工具,例如查询订单状态,然后用"金额"和"配送"两类问题检查模型是否能选对能力。把单工具流程验证清楚,再引入多步调度,会更容易判断错误来自数据、执行还是模型。

相关推荐
171320330计算机毕设编程1 小时前
计算机毕业设计 基于YOLO改进算法的车辆特征识别系统设计与实现--lw
java·大数据·python·sql·算法
y = xⁿ1 小时前
一文掌握JVM
jvm
snhna1 小时前
DeerFlow 2.x 源码拆解:多智能体委派、Skills、沙箱与 Linux 部署实战
人工智能·python
Wx-bishekaifayuan1 小时前
springboot桂味美食分享系统40540-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
kimnoic1 小时前
PHP 8.2的DNF类型怎么用才规范
开发语言·php
Metaphor6921 小时前
如何快速拆分超大 Excel 文件【Python 教程】
python·excel
_Evan_Yao2 小时前
前后端联调怎么排查问题?从 Network 到后端日志,一步步定位错误
开发语言·后端·团队开发·需求分析
hudd6682 小时前
JavaEE 初阶 - 多线程(进阶)
java·开发语言·java-ee
孟健2 小时前
从单体失控到解耦管道:用 Unix 哲学重构 Anthropic 式多 Agent 动态工作流
人工智能·python·架构