首页 / 文章 / 实用笔记:我用Ollama构建了一个本地AI智能体——以及其中的难点

实用笔记:我用Ollama构建了一个本地AI智能体——以及其中的难点

《实用笔记》操作指南:我使用 Ollama 构建了本地 AI 智能体——以及其中最棘手的部分:适用于采用该模式的团队的合约、校验机制及可直接插入的代码模块。

2143 词

本指南将逐步构建从原材料到可运行系统的完整流程,内容来自文章《我用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的批量处理注意事项:不要将提供商密钥放入代码仓库,为每个会话设置令牌使用上限,并将日志存储在评估用示例文件旁边,以便后续更换模型时保持数据可比性。