实用笔记:我用Ollama构建了一个本地AI智能体——以及其中的难点
《实用笔记》操作指南:我使用 Ollama 构建了本地 AI 智能体——以及其中最棘手的部分:适用于采用该模式的团队的合约、校验机制及可直接插入的代码模块。
本指南将逐步构建从原材料到可运行系统的完整流程,内容来自文章《我用Ollama打造了本地AI智能体——难点并不在模型本身》。重点在于可操作的步骤、明确的检查点,以及可直接放入代码仓库的代码,无需猜测其用途。 在概览阶段,应在修改代码之前明确输入参数、该步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 建议使用小型、可测试的单元而非庞大的脚本。当某个步骤失败时,故障应能指向单一责任点,而非复杂的流程链。
为何选择Ollama
在处理“为何选择 Ollama”这一阶段时,首先需列出相关契约:所需输入、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与验证后输出之间的契约。为相关成果命名,明确成功判定标准,并杜绝无声的半完成状态。 每次调用时都要记录请求编号、模型编号以及延迟时间。没有这些记录,间歇性的服务错误就会被视为应用程序的故障。
ollama pull qwen3
pip install ollama
from ollama import chat
response = chat(
model="qwen3",
messages=[
{"role": "user", "content": "Explain what an overdue invoice is."}
],
)print(response.message.content)
聊天机器人负责回答;智能体则执行操作
在开发过程中,当聊天机器人处理某个阶段时,首先需明确相关约定:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改始终符合预期。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 每次调用都要记录请求ID、模型ID以及延迟时间。如果没有这些记录,间歇性的服务提供商错误就会被视为应用程序的故障。
从简单的工具开始
在“从简单工具开始”阶段工作时,首先需明确接口规范:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持透明可追溯。 配置信息应与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中管理,以便操作人员无需查看整个系统结构即可进行审计。 每次调用时都要记录请求ID、模型ID以及响应延迟。如果没有这些记录,间歇性的服务错误就会被误认为是应用程序的缺陷。 在“从简单工具开始”阶段工作时,首先需明确接口规范:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持透明可追溯。 相比庞大的脚本,更应优先使用小型且易于测试的单元。当某个步骤出现故障时,故障原因应能明确指向某个具体的功能模块,而非整个复杂的流程。
CUSTOMERS = {
"acme plumbing": {
"customer_id": "cus_1042",
"name": "Acme Plumbing",
"email": "billing@example.com",
}
}
INVOICES = [
{
"invoice_id": "INV-2048",
"customer_id": "cus_1042",
"amount": 1850.00,
"days_overdue": 18,
}
]
def find_customer(name: str) -> dict:
customer = CUSTOMERS.get(name.strip().lower())
return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
matches = [
invoice
for invoice in INVOICES
if invoice["customer_id"] == customer_id
and invoice["days_overdue"] > 0
]
return {"invoices": matches, "count": len(matches)}
为模型提供工具,而非虚幻的访问权限
将“为模型提供工具”这一阶段视为可衡量的工作面,效果最佳。在扩大范围之前,先记录一份优秀的测试案例、一个失败案例以及回滚说明。 把这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,杜绝默许的半完成状态。 在教授循环逻辑之前,先锁定解释器及依赖项文件。在笔记本电脑与持续集成环境之间的差异是API演示中最常见的隐性故障原因。
import json
from ollama import chat
def find_customer(name: str) -> dict:
"""Find a customer by business name and return its verified record."""
customer = CUSTOMERS.get(name.strip().lower())
return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
"""Return overdue invoices for a verified customer ID."""
matches = [
invoice
for invoice in INVOICES
if invoice["customer_id"] == customer_id
and invoice["days_overdue"] > 0
]
return {"invoices": matches, "count": len(matches)}
TOOLS = {
"find_customer": find_customer,
"get_overdue_invoices": get_overdue_invoices,
}
构建智能体循环
将“构建代理循环”阶段视为可度量的对象来处理,效果最佳。在扩大范围之前,先记录一份理想的测试案例、一个失败案例以及回滚说明。在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解这些成本,就能避免在从演示环境过渡到共享环境时出现意外费用。在教授该循环之前,先锁定解释器及依赖项的配置文件。笔记本电脑与持续集成环境之间的差异是API演示中最常见的隐性故障原因。
SYSTEM_PROMPT = """
You are an invoice assistant.
Rules:
- Never invent a customer, invoice, email address, balance, or date.
- Use find_customer before requesting invoices.
- Only use customer IDs returned by tools.
- If a tool returns an error or no records, explain that clearly.
- You may draft communication, but you cannot send it.
"""
def run_agent(user_request: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_request},
] for _ in range(6):
response = chat(
model="qwen3",
messages=messages,
tools=list(TOOLS.values()),
) messages.append(response.message) if not response.message.tool_calls:
return response.message.content for call in response.message.tool_calls:
name = call.function.name
arguments = call.function.arguments if name not in TOOLS:
result = {"error": "tool_not_allowed"}
else:
try:
result = TOOLS[name](**arguments)
except (TypeError, ValueError) as error:
result = {
"error": "invalid_tool_arguments",
"detail": str(error),
} messages.append(
{
"role": "tool",
"tool_name": name,
"content": json.dumps(result),
}
) return "I stopped because the task exceeded the maximum number of steps."
真正的解决方案并非更好的提示词
真正的解决方案是:将测试环境视为可度量的对象来处理。在扩大范围之前,先记录一份理想的运行日志、一个故障案例以及回滚说明。 应将配置信息与应用程序代码分开。环境文件、密钥存储和功能开关应集中存放于一处,这样操作人员无需查看整个系统结构即可进行审计。 在讲解循环逻辑之前,先锁定解释器及依赖项。笔记本电脑与持续集成环境之间的差异是API演示中最常见的隐性故障来源。 真正的解决方案是:将测试环境视为可度量的对象来处理。在扩大范围之前,先记录一份理想的运行日志、一个故障案例以及回滚说明。 相比庞大的脚本,更应采用小型且可测试的单元。当某个步骤出现故障时,故障点应指向单一责任模块,而非复杂的流程链。
在边界处添加结构化输出
在“添加结构化输出”阶段,应在修改代码之前明确输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与已验证输出之间的契约。为相关成果命名,定义成功检测标准,并拒绝默许的半完成状态。 将客户端构建与消息循环分开,这样即便更换提供方也不必重写对话状态机。
from pydantic import BaseModel, Field
class ReminderReview(BaseModel):
customer_name: str
invoice_ids: list[str]
total_due: float = Field(ge=0)
draft_subject: str
draft_body: str
requires_approval: bool = True
review_response = chat(
model="qwen3",
messages=messages,
format=ReminderReview.model_json_schema(),
)
review = ReminderReview.model_validate_json(
review_response.message.content
)
状态与内存是不同的概念
由于状态和内存属于阶段概念,因此在修改代码之前需明确输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。应在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境切换到共享环境时出现意外费用。应将客户端构建部分与消息循环分离,这样即便更换提供方,也无需重写对话状态机。
task_state = {
"customer_id": "cus_1042",
"verified_invoice_ids": ["INV-2048"],
"approved_actions": [],
}
本地化并不等同于安全
由于Local不会自动进行阶段划分,因此在修改代码之前需明确输入参数、该步骤的负责人以及退出条件。操作员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,以便操作员无需查看整个流程即可进行审计。 将客户端构建与消息循环分开,这样就可以在不重写对话状态机的情况下更换提供者。 由于Local不会自动进行阶段划分,因此在修改代码之前需明确输入参数、该步骤的负责人以及退出条件。操作员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 相较于庞大的脚本,更应优先使用小型且可测试的单元。当某个步骤失败时,故障原因应指向单一责任模块,而非整体系统问题。
精心设计的流程。
如何测试代理
在处理“如何测试”这一阶段时,首先需明确合同条款:所需的输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与验证后输出之间的契约。为相关成果命名,定义成功检测标准,并杜绝无声的半完成状态。 每次调用时都要记录请求ID、模型ID以及延迟时间。没有这些记录,间歇性的服务错误就会被视为应用程序的缺陷。
可用版本的具体表现
在“工作版本阶段”中,首先需写下相关约定:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 每次调用都要记录请求ID、模型ID以及延迟时间。如果没有这些记录,间歇性的服务提供商错误就会被视为应用程序的故障。
User request
→ find_customer(name="Acme Plumbing")
→ verified customer_id: cus_1042
→ get_overdue_invoices(customer_id="cus_1042")
→ verified invoice: INV-2048, $1,850, 18 days overdue
→ generate draft
→ wait for human approval
最后一点总结
在完成最终课程阶段时,首先写下接口契约:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持透明可追溯。 将配置信息与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中于一个位置,以便操作人员无需查看整个系统结构即可进行审计。 在每次调用时记录请求ID、模型ID以及响应延迟。如果没有这些记录,间歇性的服务错误就会被视为应用程序的缺陷。 在完成最终课程阶段时,首先写下接口契约:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持透明可追溯。 相比庞大的脚本,应优先使用小型且易于测试的单元。当某个步骤出现故障时,故障点应能明确指向单一责任模块,而非复杂的处理流程。
运营检查清单
将操作检查清单视为可度量的基准,这样效果最佳。在扩大范围之前,先记录一份完美的操作流程、一个故障案例以及回滚说明。
同时记录正常流程与恢复流程。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的内容。
在讲解循环逻辑之前,先确定解释器版本及依赖项锁定文件。笔记本电脑与持续集成环境之间的差异是API演示中最常见的隐性故障原因。
当下一步操作涉及代码编写或工具调用时,应优先使用带有架构验证的结构化输出,而非自由形式的文字描述。
在成本较高的操作之后设置检查点。当操作员重新执行后续节点时,恢复流程不应再次调用相同的大型语言模型。
锁定依赖项版本,并记录用于演示的镜像摘要。可重复性远比团队内部经验更重要。
在推广该技术栈之前,应先冻结版本,为关键流程记录标准输出日志,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及负责密钥轮换的明确责任人。与其展示花哨的一次性演示,不如注重扎实的可靠性。
a5f763eecd03的批量处理注意事项:不要将提供商密钥放入代码仓库,为每个会话设置令牌使用上限,并将日志存储在评估用示例文件旁边,以便后续更换模型时保持数据可比性。