第 7 篇:API 调用 Agent——外部系统打通与结果解析

系列记录:《从零搭建企业级 LLM 应用》,这是第 7 篇
上一篇:数据分析 Agent——代码沙箱与 150 倍性能提升
下一篇:规则引擎——在 LLM 的输出上加一道保险


一、RAG 和数据分析之后,还缺什么

前面几篇搭好的两个 Agent

  • RAG Agent:检索的是 Dify 知识库里的文档——制度条款、操作规范、应急预案等内容型资料。它的核心能力是"从文档里找答案"。
  • Data Agent:读取的是本地上传的数据文件——Excel 考勤表、CSV 统计导出等。它的核心能力是"对已落盘的离线数据做计算"。

Dify 知识库的文档是管理员提前上传的,本地的 Excel 是用户手工导出的,相当于都是静态管理的数据,有一定的维护成本。

但在实际场景中,可能需要接入大量业务接口内的数据:

  • 实时数据和历史统计
  • 设备管理系统的传感器监测值
  • 平台的排查记录和整改状态
  • 系统业务的完成情况

这些数据的特点是:体量大、持续更新、只能通过 HTTP 接口获取。

所以典型的问题场景就出现了:

“今天数据指标有没有超标?”
“这个月违规有多少起?”
“有多少台设备处于故障状态?”

所以需要一个专门的组件,让 Agent 能调用外部 HTTP 接口获取实时结构化数据。这就是 api_agent 的定位。


二、设计方案

2.1 为什么不硬编码接口调用?

最直接的做法是在 Agent 的工具函数里硬编码 API 调用——比如写死一个 get_people_count() 函数,里面拼好 URL 和参数,调完返回结果。

但这个做法有一个根本问题:每接入一个新接口,就要改代码、重部署。而企业环境里,接口的数量和参数是持续变化的。

所以选择了配置驱动:

用户通过前端页面配置接口 → 保存为 JSON → Agent 工具读取 JSON → 动态发起 HTTP 调用

整条链路里没有一行硬编码的接口调用逻辑。新增一个接口,就是在 interface_configs.json 里加一条记录,Agent 自动就能用。

2.2 为什么没有接入 Supervisor 路由?

api_agent 和其他三个 Agent 有一个关键区别:它没有被纳入 Supervisor 的自动路由。

Supervisor agents = [rag_agent, data_agent]   # 没有 api_agent

原因很简单:实时数据查询的触发条件太模糊了。"这个月违规多不多"这句话,既可以理解为问知识库里的违规管理规定(走 rag_agent),也可以理解为查实时违规系统的统计数据(走 api_agent)。让 Supervisor 去猜用户的真实意图,准确率注定不高。

所以 api_agent 走的是显式模式切换——用户在对话页面选择"API 查询"模式,前端直接调 chat_direct("api_agent")。这和"数据统计"模式直接调 data_agent 是同一个路子。用户自己知道他想查的是实时数据还是知识库文档,不需要让 LLM 去猜。


三、两个工具的设计逻辑

api_agent 只配了两个工具,这反映了它的设计原则:能做少就做少,把复杂性压到配置层

工具一:list_interfaces —— 让 Agent 知道能调什么

list_interfaces → 读取 interface_configs.json → 返回所有已启用接口的 ID/方法/URL/参数

这个工具的设计有几个考虑:

实时读取文件,不是缓存。每次调用 list_interfaces 都重新读 JSON 文件,因为接口配置通过前端页面修改后直接写文件,热加载才能让 Agent 感知到新增或修改的接口。

分组信息带进输出。接口按"违规管理"“设备管理”“安全隐患"等分组,Agent 看到分组名就能做语义匹配——用户问"设备状态”,它自然会优先看"设备管理"分组下的接口。

工具二:call_interface —— 把配置翻译成 HTTP 调用

call_interface(interface_id, extra_params) → 合并默认参数和 extra_params → 发起 HTTP → 返回 JSON

这个工具的核心思路是参数合并策略

# 1. 默认参数(配置中已启用的)
default_params = {p["key"]: p["value"] for p in params if p["enabled"]}

# 2. extra_params 覆盖同名默认值
default_params.update(json.loads(extra_params))

用户问"最近一周的低级违规有多少"➡Agent 通过 list_interfaces 知道违规统计接口➡用自带参数覆盖默认参数➡查询接口数据


四、真实接口的复杂性:一个配置模型要应对多少种变化

interface_configs.json 定义了每条接口记录的结构

请求方式:不只是 GET

GET     → query params 拼在 URL 后
POST    → body 里放 JSON,body_type 决定数据在 body 还是 params
PUT     → 同 POST
DELETE  → query params

认证方式:不是只有无认证

# Bearer Token
auth_type = "bearer" → headers["Authorization"] = f"Bearer {auth_value}"

# API Key
auth_type = "apikey" → headers[auth_key_name] = auth_value

# Basic Auth(预留)
auth_type = "basic" → 拼接 username:password

参数状态:默认 vs 可选

enabled: true   → 每次调用自动携带,值来自配置(如 start_date=2025-01-01)
enabled: false  → Agent 可根据用户意图通过 extra_params 指定,不指定则不传

自定义请求头和 Body

接口可能要求特定的 Content-Type、自定义 Header,POST 接口的 body 可能是复杂的 JSON 结构。这些都支持在配置中声明,Agent 调用时自动拼接。


五、三个发现

配置热加载

最初考虑过启动时加载配置并缓存到内存。但使用场景中,接口配置通过前端页面随时修改——新增接口、调整参数、变更 URL。缓存会让修改延迟生效,用户困惑"明明加了接口为什么 Agent 说没有"。

所以 list_interfacescall_interface 都是每次调用时实时读文件。性能上这不是问题——读一个几 KB 的 JSON 文件是微秒级的。

处理超时和错误

调外部接口和调 Dify 知识库不同。Dify 是可控的服务,可靠性相对高。但外部接口可能是用户自己部署的系统,随时可能挂了、慢了、改了端口。

所以 call_interface 里有明确的超时和错误处理:

timeout=15   # 15 秒超时,避免长时间挂起
ConnectionError → "无法连接到 {url},请确认服务已启动"
Timeout        → "请求超时:{url}"
HTTP >= 400"接口返回错误 HTTP {status_code}:{body[:300]}"

根据不同的错误类型设置具体的错误提示词,方便排查问题。


六、与主流方案的对标

让 LLM 调用外部 API,业界有三条路线:

方案 代表 思路 适合的场景
Function Calling OpenAI, Anthropic 原生 定义 function schema,LLM 选择函数 + 填参数 固定 API 集合,接口数量少且稳定
MCP (Model Context Protocol) Anthropic 提出 标准化协议,Agent 通过 MCP Server 发现和调用工具 有现成 MCP Server 的场景
配置驱动的 HTTP 直调 本项目 JSON 配置声明接口,Agent 动态发现和调用 企业内部系统,接口多变,无 MCP Server

Function Calling 的问题在于:每个新接口要写一个函数定义 + 修改 Agent prompt + 重部署。对于频繁变动的企业接口场景,维护成本太高。

MCP 的问题在于:它要求接口提供方实现 MCP Server。企业内部系统(违规平台、设备管理、工单系统)不可能为了一个 Agent 去改造自己的协议。

所以选择了中间路线——比 Function Calling 灵活(配置即接口,无需改代码),比 MCP 轻量(不需要服务端改造)。本质上是把接口描述标准化(JSON 配置),把调用逻辑统一化(HTTP 客户端),把接口选择交给 LLM(自然语言匹配)


七、各Agent怎么配合

现在四个 Agent 各司其职:

用户问什么 走哪个 Agent 数据来源
“指数多少算超标” RAG Agent 知识库文档
“上月各部门出勤率排名” Data Agent Excel 考勤表
“现在XX指标为多少” API Agent 实时监控接口
“写一份月度报告” Doc Agent 综合以上三者

最终的用户体验是:正常聊天走知识库问答模式(Supervisor 自动路由),需要查实时数据时切换到 API 查询模式。选择权在用户,而非 LLM 猜测。


下一篇预告:规则引擎——为什么要在 LLM 的输出上加一道保险,以及怎么设计一套分层拦截的规则体系。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐