首页 / 文章

基于Ollama的本地AI代理:功能受限的工具、验证机制以及严格的步骤限制

Ollama 上的一个本地 AI 智能体,其特性包括:受限的工具列表、循环次数上限、Pydantic 结构化输出、明确的状态管理以及用于测试故障场景的测试集。

发布者:REACTAPP.TOP阅读约 9 分钟2021 词

来源: I Built a Local AI Agent With Ollama — and the Hard Part Wasn’t the Model

这款本地发票处理工具的初始版本在大约五分钟内看起来相当不错。它的任务很简单:帮助小企业主追讨逾期未付的发票。该工具可以查询客户信息,找出未支付的发票,计算逾期天数,并起草提醒邮件。测试时要求它为Acme Plumbing查找逾期发票并准备一封友好的提醒函,它立刻就回复了,附上了发票内容、未付金额以及一篇格式精美的邮件。

问题在于其中部分信息是编造的。客户确实存在,但发票编号并不存在。金额看似合理,实则错误。该工具表面上做出了成功处理的样貌,实际上并未真正完成相关工作。

那一集讲述了构建智能体时最重要的教训:听起来聪明并不等同于可靠。只有当每个重要结论都能追溯到某个受控操作或可信来源时,智能体才是可靠的。接下来的内容将使用 Ollama 和 Python 重新构建发票处理智能体,加入明确的工具、验证机制以及简洁的智能体循环结构。推理在本地机器上完成,但这些原则几乎适用于所有智能体框架。

为何选择 Ollama

本地部署可以避免在开发过程中将客户数据发送给外部模型提供商,同时还能在不重新构建应用程序的情况下切换不同的开源模型。Ollama 通过简单的聊天 API 在本地提供模型服务,还支持调用工具,这样模型就可以请求函数执行,而无需编造不存在的答案。其部署流程非常简单:

ollama pull qwen3
pip install ollama

Python 发出的普通聊天请求如下所示:

from ollama import chat
response = chat(
    model="qwen3",
    messages=[
        {"role": "user", "content": "Explain what an overdue invoice is."}
    ],
)print(response.message.content)

这样得到的只是聊天机器人,还不是智能体。

聊天机器人负责回答,智能体则按步骤执行操作

“智能体”这个词的使用较为随意。一个可行的定义是:在循环中运行的模型,能够使用各种工具,并拥有状态以及决定其下一步行动的规则。对于处理发票的任务,该循环流程如下:

  1. 用户提出请求。
  2. 模型判断是否需要使用工具。
  3. 应用程序验证工具使用请求。
  4. 应用程序执行相应的工具。
  5. 结果会被添加到对话记录中。
  6. 模型要么请求使用其他工具,要么给出最终答案。

该模型从不直接访问数据库,它只能请求应用程序调用开发者定义的工具。这一界限非常重要,因为语言模型擅长理解意图并生成文本,而应用程序代码则更擅长处理权限控制、数据查询、计算以及业务规则。

先从功能单一的工具开始

一个常见的初始错误是设计一个功能强大的单一工具,比如manage_invoices,它能实现搜索、更新、起草和发送功能。这不仅会增加调试难度,还会极大降低误用的风险。每个只承担单一任务的简单工具效果更好:

  • find_customer可将客户名称转换为真实的客户ID。
  • get_overdue_invoices会返回对应ID的逾期账单。
  • draft_reminder仅负责生成文本,不会发送任何内容。
  • send_reminder会执行实际发送操作,并需要经过审批。

简化的本地数据层提供相关数据:

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)}

看看这些函数不会做什么。它们从不猜测。缺少客户时会产生明确的错误提示,而没有匹配的发票则会产生空列表。

真正的工具,而非想象中的访问方式

Ollama将Python函数视为工具,其名称、参数和文档字符串能为模型提供选择正确函数所需的信息。

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,
}

注册表词典不仅仅是一种便利工具,实际上它是一份允许列表。即便模型请求调用名为delete_customer的函数,应用程序也会拒绝,因为TOOLS中并不存在这样的工具。

智能体循环

正是这个循环将工具调用转化为智能体行为。它始于一个系统提示:

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."

将迭代次数限制在六次是有意为之,因为智能体绝不能无限循环地反复处理同一个问题。该循环还将工具的输出视为新的证据。模型首先解析“Acme Plumbing”这一名称,获取到cus_1042,之后才会针对该已验证的标识请求发票。

更好的提示语并非解决方案

幻觉现象会诱使开发者在系统提示语中再添加一句要求,比如要求智能体必须准确无误、绝不能编造内容。这样的指令虽有一定帮助,但并不能真正起到控制作用。可靠的解决方案来自结构上的调整:

  • 数据库仍是真实数据的来源。
  • 模型可以请求数据,但无法编造出看似有效的记录。
  • 工具名称被列入允许列表,参数也会经过验证。
  • 循环有明确的步数限制。
  • 读取操作与写入操作被分开处理。
  • 发送邮件需要人工审批。
  • 提示语会影响用户行为,而代码则负责强制执行这些规则。

    边界处的结构化输出

    自由格式的文本适合用于友好的邮件沟通,但当应用程序的其他部分需要处理这些内容时则存在风险。如果系统需要向用户返回包含审核界面、发票编号、总余额以及草稿的页面,Pydantic 模型可以用来定义相应的结构:

    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
    

    Ollama 通过其 format 参数接受 JSON 模式:

    review_response = chat(
        model="qwen3",
        messages=messages,
        format=ReminderReview.model_json_schema(),
    )
    
    review = ReminderReview.model_validate_json(
        review_response.message.content
    )
    

    这能确保响应具有预期的结构。但它并不能证明这些值是正确的,因此在出现批准按钮之前,仍需将invoice_ids和total_due与实际工具的返回结果进行比对。模式验证用于检查结构是否正确,而业务验证则用于确认内容是否被允许且真实。生产环境中的系统需要两者兼备。

    状态并非内存

    长时间的对话会暴露另一个问题:模型会忘记选择了哪位客户,或者将旧发票混入新的请求中。其根源在于将整个聊天记录视为应用状态。虽然对话能提供有用的上下文,但关键状态应存储在明确的结构中:

    task_state = {
        "customer_id": "cus_1042",
        "verified_invoice_ids": ["INV-2048"],
        "approved_actions": [],
    }
    

    该模型可以总结事件,但应用程序拥有经过验证的标识符和审批权限。即便对话因上下文窗口限制而被截断,这些事实依然会保留。

    上下文长度也会影响性能,因为更长的上下文会占用更多内存并增加处理时间。无需无限制地发送所有消息和数据库记录,只需传递当前决策所需的最少信息即可。

    本地运行不等于安全

    通过 Ollama 运行模型虽能让推理过程处于本地控制之下,但并不能保障应用程序的其他部分安全。智能体仍可能将数据泄露到日志中、调用不安全的内部接口、执行文档中隐藏的恶意指令,或拥有过高的权限。仍需遵循常规的后端安全原则:

    • 仅授予每个工具其所需的权限。
    • 避免在提示词和源代码中包含敏感信息。
    • 将获取的文本视为不可信数据,绝不能当作系统指令处理。
    • 记录每次工具调用及其结果、延迟和最终决策。
    • 在数据进入模型上下文之前,先去除其中的敏感字段。
    • 对于任何外部操作或不可逆的操作,都必须获得批准。
    • 确保重要的写入操作具有幂等性,以避免重复执行。

    模型绝不能成为安全边界。

    测试

    正常流程演示会掩盖大多数智能体故障,因此值得构建一个较小的测试集。测试案例包括:一名有逾期发票的已知客户和一名没有逾期发票的已知客户、与任何人都不匹配的姓名、两个相似的公司名称、尝试发送但跳过审批的情况、注入的绕过规则的指令、格式错误的工具参数以及超时的工具。针对每个案例,需记录预期的工具、允许的操作及最终结果,并在模型、系统提示或工具描述发生变化时重新运行整套测试。

    最有用的衡量标准并非最终回复是否听起来不错,而是智能体是否按照正确的验证步骤顺序进行操作。

    可用版本

    经过重新设计后,原始请求会留下如下痕迹:

    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
    

    它没有第一个演示那么神奇,但却实用得多。一个并不存在的客户会让智能体暂停并请求进一步说明。没有过期的发票会被标记为逾期。未知的工具请求会被应用程序拒绝,而运行时间过长的循环也会安全停止。这些都不是什么光鲜亮丽的功能,但它们都在建立信任。

    最后的教训

    Ollama让运行功能强大的本地模型变得简单,但模型只是其中的一部分。真正的智能体是围绕它的整个系统:循环结构、工具契约、真实数据源、验证层、状态机、权限边界以及审批流程。

    对于初学者代理,不必使用那十种工具或完整的应用访问权限。先从一个实际问题入手,搭配两个功能有限的只读工具,记录每一个决策,补充失败案例,等到只读流程已经极其可靠之后再赋予写入权限。关键的变化在于,不再纠结于如何让模型更具自主性,而是思考如何让每一步都可观察、可验证且安全。自主性适合用于演示,而受控的自主性才能打造出实用的产品。

    相关阅读

  • DS-STAR详解:谷歌的数据科学智能体及其优势所在 — 详细介绍谷歌的DS-STAR数据科学智能体:其七个模块、DS-STAR+报告机制、已发布的提示词、消融实验结果以及现存的局限性。
  • 成本控制策略:为何AI智能体的成本会急剧上升 — 微软和Uber在智能体成本方面遇到了瓶颈。本文将解释为何智能体会如此快速消耗令牌,并介绍四种成本控制策略:路由优化、预算阈值设定、冗余删除以及人工监督。
  • 为何我们会如此震惊?AI智能体、起源与Hugging Face事件 —— 一篇探讨为何目标驱动型AI智能体突破沙箱限制早已不是新鲜事的文章,从伊甸园与弗兰肯斯坦到2026年7月发生的评估失误均有涉及。
  • 实用指南:Ornith 1.0让本地编码智能体值得再次测试 —— 对《实用指南:Ornith 1.0让本地编码智能体值得再次测试》的详细操作指引,包括合同设计、校验机制以及供采用该模式的团队直接使用的代码模板。