AI 代理要真正落地产出价值,关键点往往不在模型本身,而在于它如何安全地调用外部工具。WebMCP 这类基于 Web 的模型上下文协议封装层,正是为了解决“模型选择工具、服务执行工具、结果回到模型”这一整条链路而出现。本文用一个本地可运行的最小方案:Ollama 本地模型 + WebMCP 网关 + 工具注册中心,搭建一个 AI 代理助手。学完这个例子,你能理解工具调用的完整流程,也能把它扩展到报价计算、订单汇总、内容生成等实际业务场景。
这里的 WebMCP,指的是一种通过 Web 接口暴露模型上下文能力、让前端和业务系统以统一 HTTP 方式调用 AI 代理的实现方案。它不是某个商业产品的专有名词,不同团队对这类网关可能叫“MCP Server”“Agent Gateway”或“Tool Proxy”。文章示例使用 FastAPI 编写,本地模型通过 Ollama 启动,代码不依赖外部 SDK,方便你按自己的项目结构调整。
1. 先理解 WebMCP 在 AI 代理中扮演什么角色
1.1 模型上下文协议解决的是“工具接入”问题
AI 代理(AI Agent)不能只靠聊天完成业务任务。真正做事需要读取数据、计算金额、查询状态、写入记录,这时就必须让模型调用外部工具。早期做法是把工具说明写在系统提示词里,要求模型输出 JSON,再由代码解析执行。这种方式不稳定,模型容易把 JSON 格式写错,或者参数类型对不上,经常要反复调试。
模型上下文协议(MCP)提供了更标准的方式:客户端把“可用的工具清单”发给模型,模型在回答时如果发现需要工具,就返回结构化的tool_calls结果。服务端拿到这个结果后执行对应的函数,再把执行结果作为一条tool消息继续发给模型,让模型基于真实结果生成最终回答。
WebMCP 可以理解为“跑在 Web 层上的 MCP 网关”。浏览器或业务系统不需要直接访问本地模型、本地文件系统或命令行,只需要通过 HTTP 调用 WebMCP,WebMCP 内部再与本地模型和各个工具完成交互。这样做的价值在于:工具能力可以被统一注册、统一鉴权、统一审计,前端只需关心请求和响应。
1.2 为什么不能靠“提示词”硬编码
如果你曾经在提示词里写过“请以 JSON 格式返回”,一定遇到过这些情况:模型返回了 Markdown 代码块、属性名大小写不对、数字被写成字符串、多了一个逗号。为了让结果可用,你不得不再写一轮正则清洗和异常重试。
工具调用(function calling / tool calling)之所以更稳,是因为模型在训练和使用阶段就知道结构化的tool_calls输出格式。框架会告诉服务端“模型要求调用哪个函数、参数是什么”,服务端执行后把真实结果回填给模型。整个过程不需要从自然语言里猜 JSON,也大大减少了解析错误。
1.3 典型架构:浏览器、WebMCP、本地模型与工具
本文示例的架构分为四层:
| 角色 | 组件 | 职责 |
|---|---|---|
| 客户端 | 前端页面、curl、脚本 | 发起对话请求,传入 messages |
| WebMCP 网关 | FastAPI 服务 | 接收请求、维护工具清单、执行工具、返回最终结果 |
| 模型服务 | Ollama + 本地模型 | 理解对话、判断是否需要调用工具、生成回答 |
| 工具层 | Python 函数 | 执行计算、查询、格式化等具体动作 |
一次完整请求的链路是:用户说“500 个订单,单价 89 元,总收入多少”,WebMCP 把消息和工具清单发给本地模型,模型返回“调用calculate_revenue,参数是 500 和 89”,WebMCP 执行工具,再把结果“44500 元”交给模型,模型最终生成回答返回给用户。
1.4 适用场景与不适合的场景
WebMCP 适合以下场景:
- 自动化客服:查询订单信息、计算退款金额、生成工单摘要。
- 业务助理:根据报表数据计算营收、统计库存、生成日报。
- 内容生产:读取素材、总结摘要、生成结构化草稿。
- 企业内部知识问答:把本地文档作为工具,让模型按需检索。
不适合直接使用自动执行能力的场景包括:涉及高危操作、资金转账、删除数据、修改权限等。这些场景不是不能用 WebMCP,而是不能由模型直接执行工具,必须插入人工审批环节。
2. 环境准备:本地模型和依赖先跑通
2.1 硬件与软件要求
运行本地模型需要先确认自己的硬件条件。本文示例使用 Ollama 加载模型,推理发生在本机。
| 软件 | 建议 | 说明 |
|---|---|---|
| Python | 3.10 或更高 | WebMCP 服务运行环境 |
| Ollama | 当前稳定版 | 管理本地模型和提供 OpenAI 兼容接口 |
| 本地模型 | qwen2.5:7b、llama3.2:3b 等 | 优先选择支持工具调用的模型 |
| FastAPI | 0.100 或更高 | HTTP 网关 |
| uvicorn | 0.30 或更高 | ASGI 服务进程 |
| requests | 2.31 或更高 | 调用 Ollama 接口 |
如果内存 16GB 以上,运行 7B 量级模型比较从容。内存小于 16GB 时,建议先尝试 3B 量级模型,跑通工具调用流程后再考虑升级。
2.2 安装 Ollama 并拉取本地模型
Ollama 支持 Windows、macOS、Linux,可以到官网下载对应安装包。安装完成后先确认命令可用:
ollama --version启动服务。不同系统下服务启动方式可能不同,如果是手动启动,可以在终端执行:
ollama serve再打开一个新终端,拉取模型:
ollama pull qwen2.5:7b拉取结束后查看本机已存在的模型:
ollama list看到对应模型出现在列表中,说明模型已经就绪。
2.3 验证本地模型接口
Ollama 除了原生接口,还提供了 OpenAI 兼容接口,方便以后切换云端模型。先验证原生接口:
curl http://localhost:11434/api/tags返回 JSON 数组,里面包含已经拉取的模型名。再验证 OpenAI 兼容接口:
curl http://localhost:11434/v1/models如果两个请求都返回 HTTP 200,说明模型服务已经可供 WebMCP 使用。
2.4 学习环境与生产环境的差异
学习环境只需要让 Ollama 在本地启动,WebMCP 直接访问localhost即可。生产环境则完全不同:
- 模型服务应部署在单独主机或容器中,避免被业务代码误影响。
- WebMCP 与 Ollama 之间的调用需要网络鉴权,不能裸奔在内网。
- 本地模型需要显存和内存配置,建议先做压测,再设置合理的并发数。
- 密钥、模型路径、端口等配置必须外置,不能写死在代码里。
如果是个人学习,第 2 节完成就足够了。如果准备接业务,请在第 7 节检查生产化清单。
3. 用最小代码实现一个 WebMCP 网关
3.1 项目结构与依赖
先创建一个目录,比如webmcp-agent,里面的结构保持简单清晰:
webmcp-agent/ ├── app.py ├── ai_proxy.py ├── tool_registry.py ├── demo_tools.py └── requirements.txtapp.py是 FastAPI 入口,接收 HTTP 请求;ai_proxy.py负责和 Ollama 交互并处理工具调用循环;tool_registry.py提供工具注册和分发;demo_tools.py定义具体工具。
requirements.txt内容如下:
fastapi>=0.100 uvicorn[standard]>=0.30 requests>=2.31 pydantic>=2.0安装依赖:
pip install -r requirements.txt3.2 工具注册中心:让工具可以被模型发现
工具注册中心解决两个问题:收集工具 schema、根据名称执行对应函数。
# tool_registry.py import functools _TOOL_FUNCS = {} def tool(schema): def decorator(func): name = schema["function"]["name"] _TOOL_FUNCS[name] = (func, schema) @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator def get_schemas(): return [schema for _, schema in _TOOL_FUNCS.values()] def execute(name, **kwargs): if name not in _TOOL_FUNCS: raise ValueError(f"tool not found: {name}") func, _ = _TOOL_FUNCS[name] return func(**kwargs)这里用装饰器注册的好处是,新增工具时只需要写一个普通函数并标注 schema,后面get_schemas()会自动把工具清单传给模型。
3.3 示例工具:计算收入和查询服务器时间
为了演示工具调用闭环,这里定义两个工具。第一个是calculate_revenue,接收订单数量和单价,返回总收入,贴近业务场景。
第二个是get_server_time,返回当前服务器时间,用来验证无参工具调用。
# demo_tools.py import datetime import tool_registry @tool_registry.tool({ "type": "function", "function": { "name": "calculate_revenue", "description": "根据订单数量和单个订单的单价计算总收入,返回单位是元。", "parameters": { "type": "object", "properties": { "units": { "type": "integer", "description": "订单数量" }, "unit_price": { "type": "number", "description": "单个订单的单价,单位是元" } }, "required": ["units", "unit_price"] } } }) def calculate_revenue(units: int, unit_price: float): if units < 0 or unit_price < 0: raise ValueError("units and unit_price must be >= 0") total = round(units * unit_price, 2) return { "units": units, "unit_price": unit_price, "total_revenue": total } @tool_registry.tool({ "type": "function", "function": { "name": "get_server_time", "description": "获取 WebMCP 服务所在服务器的当前时间,返回 ISO 格式字符串。", "parameters": { "type": "object", "properties": {} } } }) def get_server_time(): return { "time": datetime.datetime.now().isoformat() }工具有两个细节值得注意:一是参数校验要放在函数内部,避免脏数据进入下游;二是返回值要序列化为可 JSON 化的结构,不能返回 Python 对象。
3.4 后端调用本地模型并处理工具调用
ai_proxy.py是核心文件。它把用户消息和工具清单一起发给 Ollama 的 OpenAI 兼容接口,如果模型返回tool_calls,则执行工具并把结果追加到消息列表,然后再请求一次模型,直到模型不再要求调用工具。
# ai_proxy.py import json import requests import tool_registry OLLAMA_BASE = "http://localhost:11434/v1" DEFAULT_MODEL = "qwen2.5:7b" MAX_ROUNDS = 5 def chat_once(model: str, messages: list): current = list(messages) for _ in range(MAX_ROUNDS): payload = { "model": model, "messages": current, "tools": tool_registry.get_schemas(), "tool_choice": "auto", } resp = requests.post( f"{OLLAMA_BASE}/chat/completions", json=payload, timeout=120, ) resp.raise_for_status() data = resp.json() message = data["choices"][0]["message"] if not message.get("tool_calls"): return {"reply": message.get("content", "")} current.append(message) for idx, call in enumerate(message["tool_calls"]): fn_name = call["function"]["name"] raw_args = call["function"].get("arguments") or "{}" if isinstance(raw_args, str): args = json.loads(raw_args) else: args = raw_args call_id = call.get("id") or f"call_{idx}" try: result = tool_registry.execute(fn_name, **args) except Exception as exc: result = {"error": str(exc)} current.append({ "role": "tool", "content": json.dumps(result, ensure_ascii=False), "tool_call_id": call_id, }) raise TimeoutError("tool call loop exceeds MAX_ROUNDS")这段代码有几个关键点:
tool_choice: "auto"表示允许模型自行判断是否需要调用工具。- 工具调用循环必须设置最大轮数,防止模型反复调用工具形成死循环。
- 工具执行错误不能直接把异常抛出,而是作为
tool消息返回给模型,让模型理解失败原因。 - 消息列表必须保持连贯,
assistant的tool_calls和tool消息要成对出现,否则接口可能报错。
3.5 FastAPI 接口:把模型能力暴露成 HTTP 服务
app.py提供三个接口:健康检查、工具列表、对话接口。
# app.py from typing import Any, Dict, List, Optional from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import demo_tools # noqa import tool_registry from ai_proxy import chat_once app = FastAPI(title="WebMCP Agent Gateway") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) class ChatBody(BaseModel): messages: List[Dict[str, Any]] model: Optional[str] = "qwen2.5:7b" @app.get("/health") def health(): return {"status": "ok"} @app.get("/tools") def list_tools(): return {"tools": tool_registry.get_schemas()} @app.post("/v1/chat") def chat(body: ChatBody): return chat_once(body.model, body.messages)启动服务:
uvicorn app:app --reload --port 8000启动后访问http://localhost:8000/docs可以看到 Swagger 文档,方便调试。
3.6 一次完整调用的预期输出
用 curl 发起请求:
curl -X POST http://localhost:8000/v1/chat \ -H "Content-Type: application/json" \ -d '{ "messages": [ {"role": "system", "content": "你是业务助理,需要使用工具回答用户问题。"}, {"role": "user", "content": "500 个订单,单价 89 元,总收入是多少?"} ] }'预期返回结构类似:
{ "reply": "500 个订单,单价 89 元,总收入是 44500.0 元。" }如果没有得到这个结果,而是模型直接输出一段文本,请检查当前模型是否支持工具调用,或者是否没有把tools参数传给 Ollama。
4. 参数配置与协议细节:为什么工具描述会影响模型行为
4.1 工具 schema 的关键字段
工具描述不是给机器看的,而是给模型看的。模型通过description判断什么时候调用哪个工具,通过parameters了解参数格式。
| 字段 | 作用 | 编写建议 |
|---|---|---|
name | 工具唯一名称 | 使用小写字母和下划线,避免特殊符号 |
description | 说明工具功能和使用条件 | 写清楚输入、输出、单位、边界条件 |
properties | 参数定义 | 每个字段都写description,说明类型和含义 |
required | 必填参数 | 只把确实必要的参数放进去,减少模型误判 |
一个常见问题是把 description 写成“计算收入”。模型看到后,不能确定自己该传哪些参数,也不知道返回什么单位。更推荐写成“根据订单数量和单个订单的单价计算总收入,返回单位是元”。
4.2 模型选择与 tool calling 能力
不是所有本地模型都支持工具调用。如果你发现模型始终不返回tool_calls,第一步不是调 WebMCP 代码,而是确认模型本身是否具备该能力。
可以到模型发布页查看是否支持 function calling 或 tool calling。实际使用中,也可以换用支持 OpenAI 兼容工具调用的模型。若模型不支持结构化工具调用,只能退化为“让模型输出 JSON 文本”的兼容方案,但那样稳定性会明显下降。
4.3 temperature、max_tokens 等参数的影响
工具调用链路里的关键参数:
| 参数 | 含义 | 推荐设置 | 错误设置的影响 |
|---|---|---|---|
temperature | 采样随机性 | 0 到 0.3 | 太高时模型可能编造工具名或参数 |
max_tokens | 单次生成最大 token 数 | 1024 以上 | 太小可能截断tool_calls |
tool_choice | 是否强制调用工具 | auto | 强制调用会降低灵活性 |
timeout | HTTP 请求超时 | 30 到 120 秒 | 太短会让本地模型来不及返回 |
在编排类任务中,temperature建议调低。因为工具参数是结构化数据,随机性太强容易导致模型“自由发挥”,把不存在的参数名传给工具。
4.4 多轮消息结构
工具调用协议要求消息列表严格区分角色:
system: 系统提示词 user: 用户问题 assistant: 模型返回的 tool_calls tool: 工具执行结果 assistant: 模型基于工具结果生成的最终回答这里最常犯的错误是:收到tool_calls后,把工具执行结果直接追加到messages,但没有先追加原来的assistant消息。很多兼容接口会因为这个原因拒绝请求。在ai_proxy.py的循环里,就是先把message追加进去,再追加工具消息,顺序不能反。
5. 运行验证与效果分析
5.1 启动服务并确认三个接口
启动uvicorn后,先确认基础接口可用:
curl http://localhost:8000/health返回:
{"status": "ok"}再查看工具清单:
curl http://localhost:8000/tools返回结果里应该包含calculate_revenue和get_server_time。如果这里没有工具,说明装饰器注册没有生效,需要检查导入顺序。
5.2 验证 AI 代理是否主动调用工具
发送一个需要计算的问题:
curl -X POST http://localhost:8000/v1/chat \ -H "Content-Type: application/json" \ -d '{ "messages": [ {"role": "system", "content": "你是业务助理,需要使用工具回答用户问题。"}, {"role": "user", "content": "帮我计算 20 个订单,单价 15.5 元,总收入是多少?"} ] }'正常链路里,模型会先返回一次tool_calls,WebMCP 执行工具后再次调用模型,最终返回“310.0 元”这类结果。为了观察这个过程,可以临时在ai_proxy.py里打印消息列表,看中间是否出现了tool_calls。
5.3 验证工具调用链路的检查顺序
如果链路不按预期工作,按以下顺序检查:
- Ollama 是否正常启动:
curl http://localhost:11434/v1/models。 /tools是否返回完整 schema。- 请求里是否带上了
tools字段。 - 模型响应里是否有
tool_calls。 - 工具函数是否抛异常。
- 最终回答是否包含“总收入”。
5.4 失败分支:工具调用异常时的表现
工具函数calculate_revenue如果收到负数参数,会主动抛出ValueError。WebMCP 会把错误转成 JSON 返回给模型。此时模型应该能基于错误信息修正回答,而不是把异常直接抛到前端。
这也是一种安全设计:工具自身要做参数校验,WebMCP 只负责把结果回传模型,不替工具做数据合法性兜底。
6. 常见问题排查:AI 代理“不听话”怎么办
6.1 模型不支持工具调用
现象:WebMCP 代码没问题,但模型返回的全部是普通文本,没有tool_calls。
原因:当前模型没有工具调用能力,或 Ollama 版本不支持该模型的工具调用。
检查方式:
ollama list查看模型名和版本,再到模型文档确认是否支持 tool calling。
解决:换用支持工具调用的模型,或者升级 Ollama 后重新拉取模型。
6.2 工具描述与模型理解不匹配
现象:模型调用了工具,但传的参数语义不对,例如把单价和数量传反。
原因:description写得太模糊,模型无法区分参数含义。
检查方式:查看/tools返回的 schema,判断描述是否足够具体。
解决:在参数描述里补充单位、边界和示例。例如“units 是订单数量,必须为非负整数”“unit_price 是单个订单的单价,单位为元”。
6.3 上下文太长被截断
现象:多轮对话后,模型突然不再调用工具,或回答内容变短。
原因:本地模型上下文窗口有限,历史消息过长后被截断。
检查方式:查看请求中的messages是否包含过多历史内容,观察模型输出是否突然中断。
解决:只保留最近几轮消息,必要时用摘要替换旧对话,或者切换上下文窗口更大的模型。
6.4 本地模型推理慢导致超时
现象:请求偶尔卡住,最终返回 HTTP 500 或请求超时。
原因:本地模型推理耗时过长,requests.post的timeout设置太短。
检查方式:直接在终端用 curl 请求 Ollama,观察第一次返回时间。
解决:把timeout调大到 120 秒,并在生产环境使用异步任务或队列,不让请求线程一直等待。
6.5 工具参数缺失或类型错误
现象:工具执行时报TypeError或missing required positional argument。
原因:模型返回的arguments是空字符串或缺少必填字段。
检查方式:打印raw_args和解析后的args,看模型生成了什么。
解决:在ai_proxy.py中做参数容错,捕获异常后把错误信息作为tool消息返回给模型,让模型重新生成参数。
6.6 工具副作用没有审计日志
现象:工具被调用后,你无法确认它执行过几次、传了什么参数。
原因:没有在工具执行入口增加日志。
检查方式:查看 WebMCP 进程日志,是否记录了工具名和参数。
解决:每次调用tool_registry.execute时,统一记录工具名、参数、执行结果和耗时。这既是排查手段,也是生产审计的基本要求。
7. 从跑通到生产:最佳实践与检查清单
7.1 学习环境与生产环境的配置对比
学习和生产环境的差异不是代码量,而是工程保障。
| 环节 | 学习环境 | 生产环境 |
|---|---|---|
| 模型部署 | 本机 Ollama | 独立容器或模型服务 |
| 鉴权 | 无 | API Key、OAuth、签名 |
| 安全 | localhost | HTTPS、网关白名单 |
| 日志 | 结构化日志 + 日志采集 | |
| 异常 | 直接抛错 | 统一错误码 + 重试策略 |
| 监控 | 无 | 请求量、延迟、工具失败率 |
| 回滚 | 重启代码 | 版本控制 + 模型版本快照 |
7.2 控制工具权限和副作用
不要把“读工具”和“写工具”混在一起。模型可以自由调用查询类工具,但写入、删除、审批类工具要加人工确认。
推荐做法:
- 工具注册时增加类别字段,区分
readonly和write。 - WebMCP 网关对写操作校验二次确认令牌。
- 所有工具调用写入审计日志。
- 不要在工具函数里硬编码数据库连接串和密钥。
7.3 请求级超时、速率限制和并发控制
本地模型一次请求可能耗时几十秒,直接用同步接口处理容易占满工作线程。
生产设计可以考虑:
客户端请求 -> 任务队列 -> AI 代理执行 -> 结果存储 -> 客户端轮询或回调同时设置单用户并发限制,避免有人连续发大量请求把本地模型打挂。当前示例是同步处理,能帮助理解原理,但上线前必须加队列和限流。
7.4 模型服务与 WebMCP 分层部署
如果想用容器统一管理,可以按这个思路编写docker-compose.yml,实际版本需要根据你的基础镜像调整:
version: "3" services: webmcp: build: . ports: - "8000:8000" environment: - OLLAMA_BASE=http://ollama:11434/v1 - DEFAULT_MODEL=qwen2.5:7b depends_on: - ollama ollama: image: ollama/ollama:latest volumes: - ollama_data:/root/.ollama ports: - "11434:11434" volumes: ollama_data:容器部署时,需要把ai_proxy.py中的OLLAMA_BASE改成环境变量,不要写死localhost。
7.5 发布前检查清单
上线前至少完成以下检查:
- 工具清单是否只包含必要工具。
- 每个工具是否都有参数校验和最大执行耗时。
- 模型是否支持工具调用。
- 写操作的二次确认机制是否生效。
- 日志是否覆盖工具名、参数、执行结果、异常。
- 请求超时、并发限制、速率限制是否已配置。
- 模型服务是否做了资源限制和监控。
- 是否已准备回滚方案,例如切回上一版本模型。
- HTTPS、鉴权、密钥管理是否到位。
- 是否对提示词注入做了基本防护,提醒模型只使用可信工具字段。
8. 扩展方向:从 AI 代理助手到业务自动化
当前示例只包含两个工具,但它们已经展现了 WebMCP 的核心能力:模型选择工具、网关执行工具、结果回传模型。沿这个方向扩展,可以接入数据库查询、订单接口、文档解析、消息推送等能力。
如果要走向更完整的业务自动化,建议按这四步推进:
- 先补充生产环境要求:队列、日志、鉴权、限流。
- 再把常见工具封装成标准函数,例如查询订单、计算金额、生成摘要。
- 给写操作增加人工审批环节,让模型只生成建议,不直接执行关键变更。
- 最后用可视化看板监控每一次工具调用,形成效果评估和问题回溯能力。
所谓“让 AI 代理为你赚钱”,本质是让代理可靠地完成可重复的增值任务,比如报价计算、订单汇总、内容生成、定时巡检。只有工具可控、结果可审计、异常可回溯,自动化才能真正成为业务资产。本文的最小示例是这一切的起点,接下来你可以在自己熟悉的业务系统里,逐步把更多工具交到 WebMCP 网关中。