把本地 LLM 变成会用工具的研究 Agent:Ollama + OpenAI Agents SDK + Tavily MCP 实战

本文整理自 Towards Data Science 文章 From Local LLM to Tool-Using Agent。原文作者 Shuai Guo 演示了一个轻量方案:用 Ollama 在本地运行 Gemma 4,再接入 OpenAI Agents SDK 和 Tavily MCP,让本地模型能搜索网页、整理证据并带引用回答问题。

你会做出什么

我们要搭一个最小可用的“本地研究 Agent”:

用户问题
  ↓
OpenAI Agents SDK 负责 agent loop
  ↓
本地 Ollama / Gemma 4 负责生成与决策
  ↓
Tavily MCP 提供网页搜索工具
  ↓
Agent 返回带来源的简短研究答案

这个方案的重点不是 Tavily 或 Gemma 4 本身,而是一个可复用模式:

适用边界

适合:

不适合直接用于:

前置条件

硬件建议:

软件准备:

安全边界:API Key 只放环境变量,不写进代码仓库。

步骤 1:安装并启动 Ollama

Linux:

curl -fsSL https://ollama.com/install.sh | sh
ollama --version

Windows 可用官方安装包,或在 PowerShell 中使用:

winget install Ollama.Ollama

Windows 安装后需要从开始菜单启动 Ollama。启动后,本地默认 API 端点通常是:

http://localhost:11434

可用下面命令做健康检查:

curl http://localhost:11434/api/version

期望看到类似 JSON:

{"version":"0.x.x"}

步骤 2:拉取本地模型

原文使用 gemma4:e4b

ollama pull gemma4:e4b

低配机器可尝试:

ollama pull gemma4:e2b

确认模型已存在:

ollama list

如果你想先测试普通生成能力:

curl http://localhost:11434/api/generate -d '{
  "model": "gemma4:e4b",
  "prompt": "Say hello in one sentence.",
  "stream": false
}'

步骤 3:创建 Python 项目

mkdir local-research-agent
cd local-research-agent
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install openai-agents openai

如果在 Windows PowerShell:

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install openai-agents openai

步骤 4:配置 Tavily MCP

Tavily 的远程 MCP URL 形如:

https://mcp.tavily.com/mcp/?tavilyApiKey=<your-api-key>

不要把 Key 写进代码。用环境变量:

export TAVILY_API_KEY="tvly-..."

Windows PowerShell:

$env:TAVILY_API_KEY="tvly-..."

步骤 5:写一个最小研究 Agent

创建 main.py

import asyncio
import os
from datetime import datetime
from urllib.parse import quote

from agents import Agent, OpenAIChatCompletionsModel, Runner
from agents.mcp import MCPServerStreamableHttp
from openai import AsyncOpenAI

MODEL_NAME = os.getenv("OLLAMA_MODEL", "gemma4:e4b")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
MAX_TURNS = 4

RESEARCH_QUESTION = (
    "Which June 23, 2026 World Cup match had the biggest group-stage stakes, "
    "and why?"
)


def build_tavily_mcp_url() -> str:
    api_key = os.environ.get("TAVILY_API_KEY")
    if not api_key:
        raise RuntimeError("Set TAVILY_API_KEY before running this script.")
    return f"https://mcp.tavily.com/mcp/?tavilyApiKey={quote(api_key)}"


def build_research_instructions() -> str:
    current_date = datetime.now().strftime("%B %d, %Y")
    return f"""
[Role]
You are a concise research assistant.

[Task]
Answer the user's question by turning it into a small web research task.
Use the current date when interpreting time-sensitive questions: {current_date}.

[Research behavior]
Start with one targeted search query.
Use follow-up searches when the first results are insufficient, conflicting, or incomplete.
Prefer relevant and credible sources.
Track which source supports each important claim.
Before answering, check whether the gathered evidence is enough to support the conclusion.

[Expected output]
Give a direct answer first, then briefly explain the evidence behind it.
Include source links for key factual claims.

[Rules]
Do not rely on memory for facts that may have changed.
Do not invent missing details.
Keep the answer concise.
""".strip()


def compact(value: object, limit: int = 220) -> str:
    text = str(value).replace("\n", " ")
    if len(text) <= limit:
        return text
    return text[:limit] + "..."


async def run_agent() -> None:
    client = AsyncOpenAI(
        api_key="ollama",
        base_url=OLLAMA_BASE_URL,
    )
    model = OpenAIChatCompletionsModel(
        model=MODEL_NAME,
        openai_client=client,
    )

    async with MCPServerStreamableHttp(
        name="tavily",
        params={"url": build_tavily_mcp_url()},
    ) as tavily_server:
        tools = await tavily_server.list_tools()
        print("Available Tavily tools:")
        for tool in tools:
            description = (tool.description or "").replace("\n", " ")
            print(f"- {tool.name}: {description[:120]}")

        agent = Agent(
            name="Local Research Agent",
            instructions=build_research_instructions(),
            model=model,
            mcp_servers=[tavily_server],
            mcp_config={"include_server_in_tool_names": True},
        )

        result = await Runner.run(agent, RESEARCH_QUESTION, max_turns=MAX_TURNS)

        print("\nTrace:")
        for step, item in enumerate(result.new_items, start=1):
            raw_item = getattr(item, "raw_item", None)
            raw_type = getattr(raw_item, "type", "")
            raw_name = getattr(raw_item, "name", "")
            raw_output = getattr(raw_item, "output", "")
            print(
                f"{step:02d} | {type(item).__name__} | "
                f"{raw_type or raw_name} | {compact(raw_output or raw_item)}"
            )

        print("\nFinal answer:")
        print(result.final_output)


if __name__ == "__main__":
    asyncio.run(run_agent())

运行:

python main.py

期望看到三类输出:

Available Tavily tools:
- tavily_search: ...

Trace:
01 | ToolCallItem | function_call | ... mcp_tavily__tavily_search ...
02 | ToolCallOutputItem | ...
03 | MessageOutputItem | message | ...

Final answer:
...

如果 trace 中出现 mcp_tavily__tavily_search,说明模型确实通过 MCP 调用了 Tavily 搜索工具。

步骤 6:加一个本地预检脚本

在真正跑 Agent 前,先检查最常见的失败点:Ollama 是否启动、模型是否已拉取、Tavily Key 是否设置。

创建 preflight.py

import json
import os
import sys
import urllib.error
import urllib.request

OLLAMA_BASE_URL = os.getenv("OLLAMA_HOST", "http://localhost:11434")
MODEL_NAME = os.getenv("OLLAMA_MODEL", "gemma4:e4b")


def fetch_json(url: str) -> dict:
    request = urllib.request.Request(url, headers={"Accept": "application/json"})
    with urllib.request.urlopen(request, timeout=5) as response:
        return json.loads(response.read().decode("utf-8"))


def fail(message: str) -> None:
    print(f"[FAIL] {message}")
    raise SystemExit(1)


def main() -> None:
    if not os.environ.get("TAVILY_API_KEY"):
        fail("TAVILY_API_KEY is not set.")

    try:
        version = fetch_json(f"{OLLAMA_BASE_URL}/api/version")
    except urllib.error.URLError as exc:
        fail(f"Cannot reach Ollama at {OLLAMA_BASE_URL}: {exc}")

    models = fetch_json(f"{OLLAMA_BASE_URL}/api/tags")
    model_names = {model.get("name") for model in models.get("models", [])}
    if MODEL_NAME not in model_names:
        fail(f"Model {MODEL_NAME!r} is not installed. Run: ollama pull {MODEL_NAME}")

    print(f"[OK] Ollama version: {version.get('version')}")
    print(f"[OK] Model installed: {MODEL_NAME}")
    print("[OK] TAVILY_API_KEY is set")


if __name__ == "__main__":
    main()

运行:

python preflight.py

预期输出:

[OK] Ollama version: 0.x.x
[OK] Model installed: gemma4:e4b
[OK] TAVILY_API_KEY is set

如何判断这个 Agent 真的在“用工具”

不要只看最终答案。至少检查这几件事:

  1. trace 里出现了 Tavily 搜索工具调用;
  2. final answer 中关键事实有来源链接;
  3. 问一个需要实时信息的问题时,模型没有只凭记忆回答;
  4. 搜索失败时,程序能报出明确错误,而不是编造结果;
  5. MAX_TURNS 足够小,避免失控循环。

原文示例中,trace 形态类似:

01 | ToolCallItem | function_call | ResponseFunctionToolCall(... name='mcp_tavily__tavily_search' ...)
02 | ToolCallOutputItem | ...
03 | MessageOutputItem | message | ... final answer ...

这比“回答看起来不错”更可靠,因为它证明了 Agent loop 至少发生过一次真实工具调用。

常见故障排查

1. Connection refused 或无法访问 Ollama

检查 Ollama 是否启动:

curl http://localhost:11434/api/version

Windows 上确认 Ollama 应用已经从开始菜单启动。

2. 找不到模型

ollama list
ollama pull gemma4:e4b

如果显存不足,尝试:

export OLLAMA_MODEL="gemma4:e2b"
ollama pull gemma4:e2b
python main.py

3. Tavily 工具不可用

检查环境变量:

echo "$TAVILY_API_KEY"

确认 Key 仍有效,并且格式正确。不要把 Key 打印到日志或提交到仓库。

4. Agent 不调用搜索工具

可以强化指令:

For any question involving current or externally verifiable facts, call the search tool before answering.

但不要把它改成“无条件搜索”。对纯本地任务,无意义搜索会增加成本和延迟。

5. 回答有来源但结论跳得太快

把输出要求改成更保守:

If evidence is weak or conflicting, say so explicitly and avoid a definitive answer.
List the source that supports each key claim.

进一步改造方向

替换模型

gemma4:e4b 不是唯一选择。你可以试:

只要能提供 OpenAI-compatible endpoint,通常就能复用同一个接入思路。

替换工具

Tavily 只是一个搜索示例。更有价值的本地化方向是:

增加安全边界

如果后续接入写操作工具,至少加这些控制:

最小验收清单

这篇文章真正值得带走的东西

这篇文章的价值不在于“用 Tavily 搜索世界杯问题”,而在于它给出了一条清晰的本地 Agent 原型路线:

本地模型 → Agent runtime → MCP 工具 → 可观测 trace → 带证据输出

先把这个链路跑通,再逐步替换模型、工具和任务类型,才是稳妥做法。不要一开始就把它包装成生产级自治系统;更合理的定位是:本地、低成本、只读、可验证的 Agent 工程实验起点。

参考来源

验证说明

本文代码片段基于原文与官方文档重构。本文生成时做了静态语法检查;未在当前机器上真实启动 Ollama、拉取 Gemma 4 或调用 Tavily API,因此不声称 real_backend_smoke_ok。真实运行前请先执行 preflight.py