第一篇:把任意 HTTP API 一键变成 Agent 工具

第零篇里,Agent 只能用本地函数干活。今天把边界推到外部世界:给它一个 OpenAPI 片段,就能自动生成可被模型"点名调用"的 HTTP 工具。重点不在花式封装,而是把"可调用契约、参数校验、错误回传"做扎实,保证模型每次出手都在你的防护网里。

先给 Agent 一个朴素但工程可用的 HTTP 执行层。我用统一返回结构把成功与失败都收敛成同一个形态,便于模型学习与我们做观测;同时限制返回大小,防止大响应淹没上下文。生产里你可以接入重试、熔断和指标,这里先把主干打通。

python 复制代码
import requests, json
from dataclasses import dataclass
from typing import Any

@dataclass
class HttpResult:
    ok: bool
    status: int
    data: Any | None
    error: str | None

def http_call(method: str, url: str, *, params=None, headers=None, json_body=None, timeout_s: int = 8) -> HttpResult:
    try:
        r = requests.request(method.upper(), url, params=params, headers=headers, json=json_body, timeout=timeout_s)
        ctype = r.headers.get("content-type", "")
        if "application/json" in ctype.lower():
            payload = r.json()
        else:
            payload = r.text[:20000]                      # 控制返回上限,避免拖垮上下文
        return HttpResult(r.ok, r.status_code, payload if r.ok else None, None if r.ok else r.text[:2000])
    except Exception as e:
        return HttpResult(False, 0, None, str(e))

模型得知道能用哪些工具、如何传参。我们沿用第零篇的思路:函数即工具,docstring 即说明;但今天的函数不是手写,而是由 OpenAPI 语义生成。为了降低门槛,我选了"只解析我们要用的那点信息"的策略:base URL、path、HTTP 方法、path/query 参数与其类型。别被完整规范吓到,能跑通 80% 的公共 API 已经够 Agent 发挥。

python 复制代码
from typing import Dict, Callable
import re

TOOLS: Dict[str, Callable[..., Any]] = {}

def tool_specs() -> str:
    lines = []
    for name, fn in TOOLS.items():
        lines.append(f"- {name}{fn.__doc__ and ':' + fn.__doc__}")
    return "\n".join(lines)

def register_openapi_tool(spec: dict, path: str, method: str, name: str | None = None) -> str:
    base = (spec.get("servers") or [{"url": ""}])[0]["url"].rstrip("/")
    op = spec["paths"][path][method.lower()]
    params = op.get("parameters", [])
    summary = op.get("summary", "").strip() or f"{method.upper()} {path}"

    tool_name = name or op.get("operationId") or f"{method.lower()}_{path.strip('/').replace('/','_')}"
    doc_param = []
    for p in params:
        t = ((p.get("schema") or {}).get("type") or "any").lower()
        need = "必填" if p.get("required") else "可选"
        loc = p.get("in")
        doc_param.append(f"{p['name']}:{t}({loc},{need})")
    doc = f"{summary}。参数:{', '.join(doc_param)}"

    def _fill_path(tmpl: str, kv: dict) -> str:
        def rep(m):
            k = m.group(1)
            if k not in kv: raise ValueError(f"缺少路径参数 {k}")
            return str(kv.pop(k))
        return re.sub(r"\{(\w+)\}", rep, tmpl)

    def _typecheck(v, t: str, n: str):
        if t == "integer" and not isinstance(v, int): raise TypeError(f"{n} 需要 integer")
        if t == "number" and not isinstance(v, (int, float)): raise TypeError(f"{n} 需要 number")
        if t == "boolean" and not isinstance(v, bool): raise TypeError(f"{n} 需要 boolean")
        if t == "string" and not isinstance(v, str): pass

    def created_tool(**kwargs):
        path_vars, query_vars = {}, {}
        for p in params:
            n, loc = p["name"], p.get("in")
            need = p.get("required", False)
            if need and n not in kwargs: raise ValueError(f"缺少必填参数 {n}")
            if n in kwargs:
                t = ((p.get("schema") or {}).get("type") or "any").lower()
                _typecheck(kwargs[n], t, n)
                if loc == "path": path_vars[n] = kwargs[n]
                elif loc == "query": query_vars[n] = kwargs[n]
        url = base + _fill_path(path, dict(path_vars))
        res = http_call(method, url, params=query_vars)
        return {"ok": res.ok, "status": res.status, "data": res.data, "error": res.error}

    created_tool.__doc__ = doc
    created_tool.__name__ = tool_name
    TOOLS[tool_name] = created_tool
    return tool_name

到这一步,任何符合"有 servers、有 paths、有 parameters"的 OpenAPI 片段都能被注册成一个工具。为了让你能当场试起来,我准备了一个极简规范片段,使用公开天气服务的查询参数语义;真实项目里你会从远端拉取 API 文档,挑你愿意暴露给模型的少数几个端点注册进去。

python 复制代码
openapi_spec = {
  "servers": [{"url": "https://api.open-meteo.com/v1"}],
  "paths": {
    "/forecast": {
      "get": {
        "summary": "查询天气预报",
        "parameters": [
          {"name": "latitude",  "in": "query", "required": True,  "schema": {"type": "number"}},
          {"name": "longitude", "in": "query", "required": True,  "schema": {"type": "number"}},
          {"name": "hourly",    "in": "query", "required": False, "schema": {"type": "string"}}
        ]
      }
    }
  }
}

tool_name = register_openapi_tool(openapi_spec, "/forecast", "get", name="weather_forecast")
print(tool_specs())

模型看见的"工具清单"此刻已经包含了 weather_forecast,docstring 里明明白白写着参数、类型和是否必填。第零篇的系统提示里只需把 tool_specs() 串起来,模型就能在需要外部信息时,生成形如 {"action":"tool","tool":"weather_forecast","args":{"latitude":..., "longitude":...}} 的调用请求。要注意的一点是,HTTP 返回常常体积巨大,不要把原样 JSON 生吞进上下文,你应该只保留关键字段或者先做一次下采样,再反馈给模型继续决策。

python 复制代码
def shrink(payload: Any, budget: int = 4000) -> Any:
    try:
        s = json.dumps(payload)
        if len(s) <= budget: return payload
        if isinstance(payload, dict):
            keep = {}
            for k in list(payload.keys())[:20]:
                keep[k] = payload[k]
            return {"__truncated__": True, "keys": list(keep.keys()), "sample": keep}
        if isinstance(payload, list):
            return {"__truncated__": True, "len": len(payload), "head": payload[:20]}
        return str(payload)[:budget]
    except Exception:
        return str(payload)[:budget]

这段压缩器不是"可有可无"的优化,而是把 HTTP→LLM 的信息通道稳定化的关键。很多人把"API 调得越多越好"当成 Agent 的聪明,现实里你需要的是"每一步都把有用的信息以可控大小回传",否则上下文反而会被噪声淹没,模型开始瞎猜。你完全可以在 created_tool 里把 res.data 先喂给 shrink 再返回,这样即使遇到异常大的 JSON,也不会拖垮循环。

在工程边界上再把两件事做硬。第一件是参数层面的自修复,当类型或必填校验失败时,不要静默失败,直接把错误打包成一个结构化的 tool 事件回消息,让模型据此改参重试。这个反馈比"自然语言报错"有效得多,因为它是可机读的、可学习的。

python 复制代码
def created_tool(**kwargs):
    try:
        path_vars, query_vars = {}, {}
        for p in params:
            n, loc = p["name"], p.get("in")
            need = p.get("required", False)
            if need and n not in kwargs: raise ValueError(f"missing:{n}")
            if n in kwargs:
                t = ((p.get("schema") or {}).get("type") or "any").lower()
                _typecheck(kwargs[n], t, n)
                (path_vars if loc=="path" else query_vars)[n] = kwargs[n]
        url = base + _fill_path(path, dict(path_vars))
        res = http_call(method, url, params=query_vars)
        data = shrink(res.data) if res.ok else None
        return {"ok": res.ok, "status": res.status, "data": data, "error": res.error}
    except Exception as e:
        return {"ok": False, "status": 0, "data": None, "error": f"arg_error:{e}"}

第二件是最小化暴露面。不要把整个 OpenAPI 丢给模型,更不要把"写操作"轻易打开。只注册你愿意承担后果的只读端点,并在 docstring 里说清可接受的参数范围。很多线上事故不是模型"变坏",而是我们"给多了"。约束就是能力。

收个尾,把这一小块拼回第零篇的闭环。System Prompt 里还是那条严格 JSON 协议,只是工具清单里多了一个 weather_forecast。当用户问"明天深圳还下雨吗",模型首先选择工具并给出参数,你执行 HTTP、裁剪响应、回填观测,再让模型根据观测收口或继续。如果今天的这条路走通了,明天你只需再注册两个端点,就能让 Agent 会查汇率、读日历或看库存,业务价值的曲线会非常陡。

下一篇换一条完全不同的路线,我会把"检索当工具而不是把长文硬塞进提示"的做法拿出来,教 Agent 用向量检索与 rerank 做信息定位,用三分钟搭一个不会幻觉的知识问答。

相关推荐
oak隔壁找我4 小时前
RabbitMQ 实现延迟通知的完整方案
java·后端
小胖霞4 小时前
从零开始:在阿里云 Ubuntu 服务器部署 Node+Express 接口(基于公司 GitLab)
前端·后端
金銀銅鐵4 小时前
[Java] JDK 21 新变化之 Sequenced Collections
后端
冯诺依曼的锦鲤4 小时前
算法练习:双指针专题
c++·算法
算家计算4 小时前
OpenAI推出首款浏览器,能否撼动全球超30亿用户的Chrome?
人工智能·openai·资讯
文火冰糖的硅基工坊4 小时前
[人工智能-大模型-33]:模型层技术 - 大模型的神经网络架构
人工智能·神经网络·架构
特拉熊4 小时前
23种设计模式之原型模式
后端·架构
trow4 小时前
ConcurrentHashMap线程安全实现详解
java·后端
trow4 小时前
HashMap核心原理与源码剖析
java·后端