引言------为什么需要让 AI 听懂 REST API
在过去两年里,我和团队接手了好几个「上了年纪」的 Java 项目。这些系统有一个共同特征:业务逻辑完整,API 齐全,Swagger 文档也写得规规矩矩,但前端换了一波人,文档却没人维护了。新来的同事要查一个订单接口,得翻好几个 YAML 文件,再对着代码比对字段名。
更要命的是,当我们打算在这些系统上引入 AI 能力时,遇到了一个非常实际的矛盾------LLM 不理解 REST API。
大语言模型固然强大,但它在推理时无法凭空知道你的订单系统有哪些接口、每个接口的入参是什么、响应结构长什么样。让它去查询订单状态,它可能会胡编一个接口路径出来。传统做法是为每个接口写工具函数(Tool),注册到 Agent 里。这个思路没错,但当你面对一个拥有上百个 REST 端点的系统时,一个一个写 Tool 注册------成本太高了。
Solon AI Harness 的 addApiServer 就是为了解决这个问题而生的。它能从 OpenAPI(Swagger)描述文件或文档 URL 中自动读取 API 定义,自动为每个端点生成 Tool 签名,Agent 直接通过自然语言调用。零代码,让大模型听懂你的 REST API。
这篇文章我会用一个完整的订单系统场景,带你从头把 OpenAPI 接入 Harness。读完你就能在自己的项目里用起来。
场景描述------一个遗留订单系统的窘境
假设我们有一个典型的电商订单系统,叫 order-service。它暴露了大概二十几个 REST 接口,涵盖订单 CRUD、库存查询、物流跟踪、售后处理等模块。这个系统有几样「遗产」:
- 接口文档存在一个内网地址
http://internal-api-doc/order-service/v3/api-docs,基于 OpenAPI 3.0 - 代码没有全部迁移到微服务,部分逻辑还是单体
- 团队想引入 AI 助手,但没人力给每个接口写 Agent Tool
这个窘境在不少 Java 项目里都出现过------API 定义得很规范,但 AI/LLM 不认识它们。
我们需要做的是:把 Harness 引擎架设在系统前面,告诉它「这是一份 OpenAPI 文档」,然后 Agent 就能听懂「帮我查一下订单 20250320001 的物流状态」这样的自然语言请求。
添加依赖
第一步,在 pom.xml 中加入 Solon AI Harness 的核心依赖。
xml
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-ai-harness</artifactId>
<version>${solon.version}</version>
</dependency>
这里 ${solon.version} 请替换成你项目实际使用的 Solon 版本,建议使用 v4.0.2 以上。Harness 本身依赖了 LLM 调用层、Agent 会话管理和 Tool 调度框架,不需要额外引入其他库。
实现------用 addApiServer 构建 AI 引擎
构建 HarnessEngine
Harness 的入口是 HarnessEngine,我们需要给它配置三样东西:
- 模型:Agent 背后的大脑,负责理解用户意图并决定调用哪个工具
- 工作目录:Harness 存放会话和缓存的工作区
- API 源:告诉 Harness 你的 OpenAPI 文档在哪
直接看代码:
java
import org.noear.solon.ai.harness.HarnessEngine;
import org.noear.solon.ai.harness.permission.ToolPermission;
import org.noear.solon.ai.chat.ChatConfig;
import org.noear.solon.ai.agent.session.AgentSessionProvider;
import org.noear.solon.ai.agent.session.InMemoryAgentSession;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class OrderAiEngine {
public static HarnessEngine create() {
// 会话提供者
AgentSessionProvider sessionProvider = new AgentSessionProvider() {
private final Map<String, AgentSession> sessionMap = new ConcurrentHashMap<>();
@Override
public AgentSession getSession(String instanceId) {
return sessionMap.computeIfAbsent(
instanceId, k -> InMemoryAgentSession.of(k)
);
}
};
// 构建引擎
HarnessEngine engine = HarnessEngine.of("work", "./harness-home")
.systemPrompt("你是一个订单系统 AI 助手。" +
"你可以查询订单、查询库存、跟踪物流、处理售后。" +
"使用 OpenAPI 工具时按照文档说明传递参数。" +
"如果用户意图不明确,主动追问确认。")
.sessionProvider(sessionProvider)
.modelAdd(new ChatConfig().then(slf -> {
slf.setApiUrl("https://api.openai.com/v1");
slf.setApiKey("sk-xxxxxx");
slf.setModel("gpt-4o-mini");
}))
.sandboxEnabled(true);
return engine;
}
}
几个关键点:
HarnessEngine.of(workspace, harnessHome)需要两个目录参数,workspace 定位工作目录,harnessHome 放配置和框架产物systemPrompt用来约束 Agent 行为,会注入到每次对话的系统消息中- 生产环境建议开启
sandboxEnabled(true),避免 Agent 执行危险操作
接入 OpenAPI------addApiServer 的核心用法
现在是最关键的一步,让 Harness 理解你的 REST API。
java
import org.noear.solon.ai.harness.source.ApiSource;
// 在引擎上注册 OpenAPI 源
engine.addApiServer(new ApiSource().then(s -> {
s.setDocUrl("http://internal-api-doc/order-service/v3/api-docs");
s.setApiBaseUrl("http://order-service.internal:8080");
}));
就这么简单。ApiSource 通过 setDocUrl 指定 OpenAPI 文档地址,setApiBaseUrl 指定实际调用的 Base URL。Harness 会自动做以下几件事:
- 拉取 OpenAPI 文档(支持 JSON 和 YAML,兼容 OpenAPI 3.0 / 3.1 / Swagger 2.0)
- 解析每个路径、方法、参数、请求体和响应结构
- 自动生成对应的 Tool 描述和参数签名
- 注册到 Agent 的可用工具列表
整个过程完全不需要你写一行工具代码。二十个接口如此,两百个接口也是如此。
完整的引擎初始化
java
public static HarnessEngine createEngine() {
AgentSessionProvider sessionProvider = new AgentSessionProvider() {
private final Map<String, AgentSession> sessionMap = new ConcurrentHashMap<>();
@Override
public AgentSession getSession(String instanceId) {
return sessionMap.computeIfAbsent(
instanceId, k -> InMemoryAgentSession.of(k)
);
}
};
HarnessEngine engine = HarnessEngine.of("work", "./harness-home")
.systemPrompt("你是一个订单系统 AI 助手。" +
"你可以查询订单、查询库存、跟踪物流。" +
"始终使用 OpenAPI 工具获取实时数据。" +
"如果参数不完整,询问用户补充。")
.sessionProvider(sessionProvider)
.modelAdd(new ChatConfig().then(slf -> {
slf.setApiUrl("https://api.openai.com/v1");
slf.setApiKey("sk-xxxxxx");
slf.setModel("gpt-4o-mini");
}))
.sandboxEnabled(true)
.memoryEnabled(true)
.maxTurns(10)
.compressionThreshold(30, 30_000)
.addApiServer(new ApiSource().then(s -> {
s.setDocUrl("http://internal-api-doc/order-service/v3/api-docs");
s.setApiBaseUrl("http://order-service.internal:8080");
}));
return engine;
}
memoryEnabled(true):Agent 在多轮对话中记住上下文maxTurns(10):限制对话最大轮次compressionThreshold(30, 30_000):超过 30 条消息或 30000 token 时自动压缩
实战------用自然语言查询订单和库存
引擎搭好了,来看实际对话效果。
场景一:查订单
java
public void demoQueryOrder() {
HarnessEngine engine = createEngine();
String question = "帮我查一下订单 20250320001 的物流状态,看看今天能不能到。";
var result = engine.prompt(question).call();
System.out.println(result.getContent());
}
引擎收到这个 prompt 后,内部工作流程如下:
- 大模型判断意图属于「查询订单物流」
- 从 OpenAPI 生成的 Tool 列表中选中对应的
GET /api/orders/{orderId}/logistics - LLM 自动提取参数
orderId = "20250320001" - Harness 组装 HTTP 请求
- 拿到响应 JSON 后,LLM 把结构化数据转成自然语言回答
输出类似:
diff
订单 20250320001 的物流状态如下:
- 承运方:顺丰速运
- 运单号:SF1234567890
- 当前状态:运输中
- 最新节点:2025-03-20 14:30,已到达【上海市浦东新区分拨中心】
- 预计送达:今天(3月21日)18:00 前
目前看今天能到,建议您保持电话畅通。
场景二:查库存 + 组合意图
java
String question = "SKU 88002 还有库存吗?如果缺货的话,什么时候能补上?";
var result = engine.prompt(question).call();
Harness 会自动判断需要调用 GET /api/inventory/{sku} 这个接口。如果 OpenAPI 中还有一个补货计划接口,初次查询发现库存为 0,LLM 会主动发起第二次工具调用查询补货计划,然后整合给出回答。
objectivec
SKU 88002(商品:极简蓝牙耳机)当前库存为 0。
根据补货计划,下一批预计在 3 月 25 日到货,数量 500 件。建议您开启到货提醒。
LLM 不是一次把所有工具都调一遍,而是根据上下文的实际需要动态决定调用顺序。
场景三:多轮会话
java
public void demoMultiTurn() {
HarnessEngine engine = createEngine();
AgentSession session = engine.getSession("order-002");
var r1 = engine.prompt("帮我查一下订单 20250320001 的物流")
.session(session).call();
System.out.println(r1.getContent());
// Agent 记得上文讨论的是同一个订单
var r2 = engine.prompt("这个订单什么时候能到?")
.session(session).call();
System.out.println(r2.getContent());
}
关键点在于 session 对象------Harness 会把历史消息存到 session 中,LLM 看到上文就知道「这个订单」指的是 20250320001。
进阶------注册自定义业务工具补充 OpenAPI
addApiServer 解决了 80% 的通用问题,但总有一些逻辑无法用纯粹的 REST API 表达。比如一个「极速发货」操作,内部涉及:检查支付状态 → 检查库存 → 调拨 → 更新状态 → 发通知,不适合暴露为单个 REST 端点。
这时可以用 extensionAdd 注册自定义 Tool:
java
import org.noear.solon.ai.harness.tool.*;
public class ExpeditedShippingTool extends AbstractTool {
public ExpeditedShippingTool() {
super("expedited_shipping", "对已支付的订单执行极速发货。需要订单 ID 和发货仓库。");
}
@Override
public ToolResult execute(Map<String, Object> args) {
String orderId = (String) args.get("orderId");
String warehouse = (String) args.get("warehouse");
// 业务逻辑...
return ToolResult.success("订单 " + orderId + " 已执行极速发货。");
}
}
// 注册到引擎
engine.extensionAdd((agentName, agentBuilder) -> {
agentBuilder.defaultToolAdd(new ExpeditedShippingTool());
});
注册后,Agent 的工具列表就变成了「OpenAPI 自动生成的接口」+「自定义 Tool」,LLM 会根据意图自动选择用哪个。
最佳实践
经过多个项目的落地,我总结了几条实用的经验:
1. 文档质量决定 Agent 表现
addApiServer 的输入是 OpenAPI 文档,输出就是 Agent 的工具集。每个接口的 summary 和 description 要写清楚,参数要有明确的 description,标明格式、范围、是否必填。
yaml
/api/orders/{orderId}:
get:
summary: 查询订单详情
description: 根据订单 ID 获取订单完整信息,包括商品、金额、状态等。
parameters:
- name: orderId
in: path
required: true
description: 订单编号,格式为 YYYYMMDD + 6 位流水号
schema: { type: string }
2. 设置合理的系统提示词
systemPrompt 建议包含:角色定义、能力边界、输出规范、安全约束。
3. 生产环境务必开启沙箱
java
.sandboxEnabled(true)
沙箱会限制 Agent 的工具调用范围,防止 LLM 幻觉导致意外调用写接口。
4. 合理使用会话管理
面向最终用户时,建议用 AgentSessionProvider 实现持久化存储。同时设置 compressionThreshold 避免长对话撑爆上下文。
5. 自定义 Tool 粒度适中
一个 Tool 不要做太多事情,否则 LLM 难以理解。反过来,也尽量不要一个接口一个 Tool------既然有 addApiServer 了,没必要重复造轮子。自定义 Tool 只封装那些需要业务逻辑组合或特殊权限控制的操作。
总结
这篇文章从实际问题出发,完整走了一遍 Solon AI Harness 接入 OpenAPI 的流程。
回顾核心要点:
addApiServer是零代码方案,传入 OpenAPI 文档地址,自动生成 Tool,Agent 直接用自然语言调用- 引擎配置灵活 ,通过
systemPrompt、modelAdd、sessionProvider等 API 精确控制 Agent 行为 - 多轮会话和记忆管理让 Agent 具备上下文感知能力
extensionAdd提供扩展点,当 OpenAPI 覆盖不了业务场景时,可以注册自定义 Tool- 最佳实践集中在文档质量、提示词设计、安全沙箱三个方面
在我带团队落地 Harness 的几个项目中,addApiServer 是让大家最「爽」的功能。以前接入一个十几接口的系统,配置 Tool 要半天;现在改两行配置、传一个文档 URL 就搞定了。更重要的是,API 变化时不需要改 Agent 代码------更新 OpenAPI 文档就行了。
如果你手头正有一个带 Swagger/OpenAPI 文档的 Java 系统,强烈建议试试用 Harness 架一层 AI 能力。你会惊讶地发现,让大模型「听懂」你的 REST API,原来可以这么简单。
参考文档: