把 Agent Skill 包成 API:从本地调用到服务化落地

## 引子

前面我们聊过 Skill 是什么,也聊过怎么写一个可复用的 Skill。

但只在本地 Agent 里能用,其实还不够。

很多真正有价值的 Skill,最后都会遇到同一个问题:**我已经把业务流程沉淀成 Skill 了,能不能让 Java 后端、小程序、内部系统也调用它?**

比如你写了一个`weather-query`Skill,里面已经定义好了:

* 什么时候把城市名解析成经纬度
* 什么时候查天气预报接口
* 什么时候读取天气字段说明
* 什么时候给出出行或跑步建议
* 最终回答应该按什么格式返回

本地 Agent 能跑通,说明这套流程是有价值的。但如果只有你自己的电脑能用,它就还是一个“个人工具”;如果能包装成 HTTP API,它才开始变成一个“团队能力”。

这篇文章就围绕这个问题展开:**如何把我们写的 Skill,不只是给本地 Agent 调用,而是包装成 API,对外提供服务。**

先看最终接口效果预览:

`GET /query/stream?question=北京明天适合户外跑步吗?`
服务端通过 SSE 持续推送事件,调用方只需要监听最终的`final`事件:

`event: startdata: {“success”:true,”message”:”agent query started”,”request_id”:”req_123″}event: progressdata: {“message”:”正在读取 Skill”,”step”:”read_skill”}event: finaldata: {“success”:true,”answer”:”来源: Open-Meteon建议: 明天上午更适合跑步…”,”source”:”langgraph-agent”,”error”:null,”meta”:{“elapsed_ms”:12345}}event: donedata: {“success”:true}`
后面我们就围绕这个接口,拆清楚为什么要这么设计、具体怎么实现,以及最后怎么测试通过。

本文目录:

一、为什么 Skill 需要 API 化

二、Skill 对外提供 API 的几种方式

三、LangGraph 到底是什么

四、为什么推荐 LangGraph Agent Runtime

五、整体架构怎么设计

六、关键实现细节和原理

七、一次请求是怎么跑完的

八、用一个例子测试通过

九、总结

* * *

## 一、为什么 Skill 需要 API 化

先说结论:**Skill 的价值不只是“提示词复用”,而是“业务能力复用”。**

一个成熟的 Skill,通常不只是几段提示词。它往往包含三类东西:

`Skill = 工作流程 + 参考资料 + 可执行脚本`
以`weather-query`为例,它可能长这样:

`skills/└── weather-query/    ├── SKILL.md    ├── references/    │   └── open-meteo-api.md    └── scripts/        ├── weather-api/        │   └── query.py        └── geocoding/            └── query.py`
其中:

* `SKILL.md`是工作流说明,告诉 Agent 先做什么、后做什么;
* `references/`是上下文资料,比如接口文档、字段说明、口径规则;
* `scripts/`是确定性执行代码,比如请求天气接口、请求地理编码接口。

这已经不是一个简单提示词了。它更像一个“会读文档、会调用工具、会组织回答的小型业务专家”。

问题来了:业务系统要怎么用它?

如果一个 Java 后端想回答“北京明天适合户外跑步吗?”,它并不想知道:

* 城市名怎么转经纬度;
* 天气接口叫什么;
* 小时级降雨概率、风速、体感温度字段怎么取;
* 返回结果怎么解释;
* 没有数据时应该怎么回答。

Java 后端最理想的调用方式是:

`GET /query/stream?question=北京明天适合户外跑步吗?`
然后通过 SSE 监听最终结果:

`event: finaldata: {“success”:true,”answer”:”来源: Open-Meteon查询意图: …”,”source”:”langgraph-agent”,”error”:null}event: donedata: {“success”:true}`
这就是 Skill API 化的核心目标:

> 把本来只能在 Agent 环境里触发的业务能力,封装成稳定的 HTTP 服务,让普通业务系统也能调用。

* * *

## 二、Skill 对外提供 API 的几种方式

要把 Skill 变成 API,大体有三种路线。

### 2.1 方案一:服务器上安装 Codex 这类 Agent

最直观的方式是:既然本地 Codex 能调用 Skill,那就在服务器上也安装 Codex 或类似 Agent,然后 API 服务收到请求后,去驱动这个 Agent。

流程大概是:

`调用方  |  vHTTP 服务  |  v服务器上的 Codex / Agent  |  v加载本地 Skill  |  v执行任务并返回结果`
这个方案的优点是上手快。你本地怎么跑,服务器上大体也可以照着配置。

但它也有几个问题:

| 维度 | 服务器安装 Agent 方案 |
| — | — |
| 优点 | 改造少,最接近本地使用方式 |
| 缺点 | 服务边界不清晰,进程管理、并发、权限控制更复杂 |
| 适合场景 | 内部低频工具、个人自动化、快速验证 |

这里最大的风险是:**Agent 本身不是为稳定 HTTP 服务边界设计的。**

它能完成任务,但你还要额外处理很多服务化问题,比如并发请求、超时控制、日志、鉴权、文件权限、错误码转换等。

所以这个方案适合做验证,不一定适合长期承载业务接口。

### 2.2 方案二:把 Skill 逻辑重写成普通后端代码

第二种方式是更传统的后端思路:直接把`SKILL.md`里的流程翻译成 Java、Python 或 Go 代码。

比如:

`如果问题命中天气查询  -> 先做城市地理编码  -> 再调天气预报接口如果城市无法识别  -> 返回澄清问题最后按固定模板给出天气建议`
这个方案看起来很稳,因为它完全变成了普通工程代码。

但问题也很明显:**你失去了 Skill 的灵活性。**

Skill 原来最有价值的地方,是把“流程说明、接口文档、判断策略、输出要求”放在一个 Agent 能理解的工作包里。现在你把它重新写死在后端代码里,后续每次调整业务规则,都要改代码、发版、回归。

这就有点可惜了。

### 2.3 方案三:用 LangGraph 实现 Agent Skills 架构

第三种方式,也是这篇文章重点讲的方式:

> HTTP 服务本身不重写 Skill 业务流程,而是实现一个轻量 Agent Runtime。Runtime 读取完整 Skill 包,让 Agent 按`SKILL.md`决策、读资料、调脚本、组织答案。

也就是说,我们不是把 Skill “翻译成代码”,而是把 Skill “放进一个可服务化的 Agent 运行时”。

整体感觉像这样:

`调用方  |  vFastAPI /query/stream  |  vLangGraph Agent Runtime  |  v读取 SKILL.md / references / scripts  |  v调用脚本查询业务接口  |  v持续返回 SSE 事件,最终通过 final 事件给出 answer`
这个方案的关键点是:**HTTP 服务只负责运行时能力,业务流程仍然留在 Skill 包里。**

它既保留了 Skill 的可维护性,也把对外服务边界做清楚了。

* * *

## 三、LangGraph 到底是什么

说到这里,有些同学可能会问:LangGraph 是什么?它和 LangChain 又是什么关系?

先不用被名字吓到。通俗地说:

> LangGraph 是一个用来搭建 Agent 工作流的编排框架。它把一次复杂的 AI 任务,拆成多个节点、状态和工具调用,再按一定路线跑完整个流程。

如果说普通的大模型调用像这样:

`用户问题 -> 模型 -> 回答`
那 LangGraph 更像这样:

`用户问题  |  v节点 A:理解意图  |  v节点 B:选择工具  |  v节点 C:调用外部脚本或接口  |  v节点 D:汇总结果并回答`
它的核心不是“多调几次模型”,而是把 Agent 的行动过程变得更可控。

一个典型 LangGraph 应用里,通常会有这几个概念:

| 概念 | 可以怎么理解 |
| — | — |
| State | 当前任务的上下文,比如用户问题、工具结果、历史消息 |
| Node | 一个处理步骤,比如调用模型、执行工具、整理答案 |
| Edge | 步骤之间怎么流转,比如执行完工具后回到模型继续判断 |
| Tool | Agent 能调用的外部能力,比如读文件、查接口、跑脚本 |
| ReAct Agent | 一种常见模式:模型先推理,再选择工具,再根据工具结果继续推理 |

为什么这对 Skill API 很重要?

因为 Skill API 不是简单的问答接口。它需要让 Agent 先读`SKILL.md`,再按需读`references/`,然后执行`scripts/`,最后组织答案。

这天然就是一个多步骤流程。

如果只用普通的模型调用,你要自己维护这些步骤之间的状态和循环;用 LangGraph,就可以把这个过程变成一个明确的 Agent Runtime。

简单来说:

`普通模型调用:适合一次问答LangGraph:适合多步骤、有工具、有状态的 Agent 任务`
这也是为什么本文会选择 LangGraph 来包装 Skill,而不是只写一个普通的 LLM 调用接口。

* * *

## 四、为什么推荐 LangGraph Agent Runtime

LangGraph 的价值不在于“多包装一层框架”,而在于它天然适合做 Agent 工作流。

一个 Skill API 服务至少需要这些能力:

* 能给模型一个明确的 system prompt;
* 能让模型根据用户问题选择工具;
* 能限制工具权限;
* 能控制最多执行多少步;
* 能在工具调用和最终回答之间保留状态;
* 能把 Agent 最后一条回答提取出来返回给 API。

这和 LangGraph 的 ReAct Agent 模式非常契合。

我们可以把 Skill 服务理解成一个小型运行时:

`LangGraph Agent Runtime├── 模型:负责理解问题、规划步骤、组织回答├── 文件工具:负责读取 SKILL.md 和 references├── 脚本工具:负责执行 skill/scripts 下的确定性代码└── 控制层:负责超时、步数、错误和返回格式`
这里有一个很重要的设计原则:

> 模型负责“判断和编排”,脚本负责“确定性执行”。

千万不要让模型直接猜接口参数,也不要让模型直接拼复杂业务请求。

更稳的方式是:

* 接口调用、鉴权、分页、字段映射,放到`scripts/`里;
* 判断何时调用哪个脚本,放到`SKILL.md`里;
* HTTP 服务只提供工具和运行环境。

这样边界才清楚。

* * *

## 五、整体架构怎么设计

根据`skill-api/README.md`里的方案,我们可以把整个系统拆成四层:

`调用方(Java后端/小程序后端)        |        | HTTP GET /query/stream (SSE)        vFastAPI Controller        |        | 参数校验、错误码转换        vLangGraph Agent Service        |        | 读取 system prompt + 用户问题        vLangGraph ReAct Agent        |        | tool call        +—————————–+        |                             |        v                             vSkill 文件工具                  Skill 脚本执行工具read_skill_file                 run_skill_scriptlist_skill_files                       |        |                              v        v                        scripts/weather-api/query.pySKILL.md / references/          scripts/geocoding/query.py        |                              |        +————–+—————+                      v              Agent 汇总最终回答                       |                       v              FastAPI 持续返回 SSE 事件                       |                       v              调用方监听 final 事件并展示 answer`
这张图里最关键的是两个工具:

* `read_skill_file`:让 Agent 读取 Skill 包内的说明和参考资料;
* `run_skill_script`:让 Agent 执行 Skill 包内的脚本。

也就是说,Agent 并不是凭空“脑补”答案,而是按 Skill 包里的资料和脚本行动。

这和传统 Java Service 可以类比:

| Java 后端概念 | Skill API 方案 |
| — | — |
| Controller | FastAPI`/query/stream` |
| Service | `run_agent_query` |
| 策略编排代码 | LangGraph ReAct Agent |
| 接口文档/流程说明 | `SKILL.md`+`references/` |
| DAO/Client | `scripts/weather-api/query.py`、`scripts/geocoding/query.py` |
| 第三方接口 | Open-Meteo 天气预报 API、Open-Meteo 地理编码 API |

区别在于:传统 Java Service 把流程写死在代码里;Skill API 把流程留在`SKILL.md`,由 Agent 运行时读取执行。

* * *

## 六、关键实现细节和原理

接下来我们看几个真正落地时绕不开的细节。

### 6.1 Skill 包必须保持完整目录结构

不要只复制`SKILL.md`。

一个可服务化的 Skill 应该作为完整目录部署:

`skills/weather-query/├── SKILL.md├── references/└── scripts/`
原因很简单:Skill 不是孤立提示词。

`SKILL.md`可能会要求 Agent 读取`references/open-meteo-api.md`,也可能会要求执行`scripts/weather-api/query.py`。如果只拿走一个 Markdown 文件,运行时就失去了上下文和执行能力。

所以部署时要保证:

* `SKILL.md`、`references/`、`scripts/`在同一个 skill 根目录;
* HTTP 服务通过`WEATHER_SKILL_DIR`定位这个目录;
* 新增业务能力优先改 Skill 包,而不是改服务框架。

### 6.2 System Prompt 要强制 Agent 先读 Skill

如果只是把用户问题丢给模型,它可能会直接回答,而不是按 Skill 流程走。

所以在`run_agent_query`里,需要构造明确的 system prompt:

`你必须使用 Agent Skills 架构。处理用户问题前,先读取 SKILL.md。如 SKILL.md 要求读取 references,请继续读取相关资料。如需要访问业务数据,只能通过 run_skill_script 执行 scripts/ 下的脚本。不要直接编造接口结果。`
这段话的作用不是“装饰”,而是在约束 Agent 的执行路径。

模型可以推理,但不能绕开 Skill 包。

### 6.3 文件读取工具必须做路径保护

这是服务化后非常重要的安全细节。

如果你给 Agent 一个任意文件读取工具,它理论上可能读取到服务目录外的配置文件、密钥、日志等敏感内容。

正确做法是:文件工具只能访问 skill 根目录内的文件。

伪代码大概是:

`def resolve_skill_path(relative_path: str) -> Path:    target = (SKILL_DIR / relative_path).resolve()    skill_root = SKILL_DIR.resolve()    try:        target.relative_to(skill_root)    except ValueError:        raise ValueError(“只能读取 skill 目录内的文件”)    return target`
实际实现建议用`Path.relative_to`做目录包含判断,而不是简单判断字符串前缀。否则`/tmp/skill`和`/tmp/skill-evil`这类路径,很容易出现前缀误判。

这里的重点不是代码写法,而是原则:

> Agent 能力越强,工具边界越要收紧。

### 6.4 脚本执行工具也要限制范围

同样,`run_skill_script`不能变成一个万能 shell。

它应该只允许执行:

* `scripts/`目录下的文件;
* 后缀为`.py`的脚本;
* 固定工作目录为 skill 根目录;
* 固定超时时间;
* 返回`stdout`、`stderr`、`returncode`。

执行结果可以像这样返回给 Agent:

`{  “returncode”: 0,  “stdout”: “{“items”:[…]}”,  “stderr”: “”}`
如果脚本报错,也不要直接把敏感信息原样返回。至少要对包含这些关键词的行做脱敏:

* `token`
* `authorization`
* `api_key`
* `secret`

这个细节很容易被忽略,但一旦服务对外提供,就必须考虑。

### 6.5 模型配置和业务接口配置要分离

这个方案里有两类配置。

第一类是 Agent 模型配置:

`{  “provider”: “openai-compatible”,  “model”: “your-agent-model”,  “base_url”: “https://api.example.com/v1”,  “wire_api”: “chat_completions”,  “api_key”: “你的模型 API Key”}`
它影响的是:Agent 用哪个模型、怎么调用模型、温度多少、最多跑多少步。

第二类是业务接口配置:

`WEATHER_API_URLWEATHER_API_TIMEOUTGEOCODING_API_URLGEOCODING_API_TIMEOUTGEOCODING_LANGUAGE`
它影响的是:脚本如何访问天气预报和地理编码服务。

这两类配置不要混在一起。

`Agent 模型配置  -> server/main.py  -> ChatOpenAI / LangGraph Agent业务接口配置  -> skills/weather-query/scripts/*/config.json  -> query.py  -> Open-Meteo 天气 API / 地理编码 API`
这样做的好处是很明显的:

* 换模型,不影响业务接口脚本;
* 换天气服务地址,不影响 Agent Runtime;
* 本地配置和部署配置可以通过环境变量覆盖。

* * *

## 七、一次请求是怎么跑完的

我们把完整请求链路串起来。

用户从业务系统发起请求:

`GET /query/stream?question=北京明天适合户外跑步吗?`
第一步,FastAPI Controller 接收 SSE 请求,校验`question`不能为空。

第二步,Controller 调用`stream_agent_query(question)`,先向调用方推送`start`事件。

第三步,Service 检查 Skill 包:

`skills/weather-query/SKILL.md 是否存在skills/weather-query/scripts/ 是否存在skills/weather-query/references/ 是否存在`
第四步,Service 构造 system prompt,并启动 LangGraph Agent。

第五步,Agent 首先调用:

`read_skill_file(“SKILL.md”)`
第六步,Agent 根据`SKILL.md`判断:这个问题属于天气数据查询,需要读取接口说明。

于是继续调用:

`read_skill_file(“references/open-meteo-api.md”)`
第七步,Agent 决定先调用地理编码脚本,把城市名转换成经纬度:

`run_skill_script(  “scripts/geocoding/query.py”,  [“北京”])`
然后调用天气查询脚本:

`run_skill_script(  “scripts/weather-api/query.py”,  [“–latitude”, “39.90”, “–longitude”, “116.40”, “–date”, “tomorrow”])`
第八步,如果天气 API 返回有效数据,Agent 按 Skill 要求组织回答。

如果城市无法识别,Agent 不继续猜测,而是返回澄清问题,比如“你说的是北京,中国,还是其他同名城市?”

第九步,Agent 输出最终回答。

第十步,FastAPI 通过 SSE 推送最终事件:

`event: finaldata: {“success”:true,”answer”:”来源: Open-Meteon查询意图: 北京明天户外跑步天气查询n建议: 明天上午更适合跑步,注意风速和体感温度…”,”source”:”langgraph-agent”,”error”:null,”meta”:{“skill_path”:”…/skills/weather-query”,”elapsed_ms”:12345,”recursion_limit”:30}}event: donedata: {“success”:true}`
这就是一次完整闭环。

这里为什么要从普通`POST /query`改成 SSE?

核心原因是:**Agent 任务不是普通数据库查询,它的执行时间和中间步骤都不稳定。**

一次 Skill 调用可能要经历读`SKILL.md`、读参考文档、调用脚本、等待外部 API、失败重试、再组织回答。同步 JSON 接口要等所有步骤结束后才返回,调用方在这段时间里只能干等。如果执行超过网关或浏览器的超时时间,还容易被误判为失败。

SSE 更适合这种场景:

* 连接建立后,服务端可以先推`start`,告诉调用方任务已经开始;
* 中间可以推`progress`,告诉前端“正在读取 Skill”“正在调用脚本”;
* 成功时推`final`,调用方拿到`answer`后展示;
* 最后推`done`,明确这次流式响应结束;
* 失败时推`error`,错误也能用统一事件格式处理。

它比 WebSocket 更轻量,也比普通同步接口更适合“服务端持续输出、客户端只负责监听”的 Agent 查询场景。

事件协议建议提前约定清楚:

| 事件 | 含义 | 关键字段 |
| — | — | — |
| `start` | 任务开始 | `success`、`message`、`request_id` |
| `progress` | 中间进度 | `message`、`step` |
| `final` | 最终答案 | `success`、`answer`、`source`、`error`、`meta` |
| `error` | 执行失败 | `success`、`error`、`code`、`meta` |
| `done` | 流结束 | `success` |

这里还有个坑,大家一定要注意:`GET /query/stream?question=…`很适合演示和轻量调用,但生产环境要处理三个问题。

第一,`question`必须做 URL 编码,中文、空格和特殊符号不能直接裸传。

第二,GET 参数通常会进入浏览器历史、网关日志和访问日志,所以不适合传敏感问题。

第三,URL 有长度限制。问题很长时,建议保留 SSE 思路,但改成 POST 流式响应;或者先创建任务拿到`query_id`,再用`GET /query/stream?query_id=…`监听结果。

* * *

## 八、用一个例子测试通过

最后看怎么验证这个服务。

先安装依赖:

`python3 -m venv .venv.venv/bin/python -m pip install -r requirements.txt`
准备 Agent 配置:

`cp config/agent.example.json config/agent.local.json`
配置内容示例:

`{  “provider”: “openai-compatible”,  “model”: “your-agent-model”,  “base_url”: “https://api.example.com/v1”,  “wire_api”: “chat_completions”,  “api_key”: “你的模型 API Key”}`
然后启动服务:

`HOST=0.0.0.0 PORT=8000 .venv/bin/python -m server.main`
先检查健康状态:

`curl http://127.0.0.1:8000/healthz`
如果返回类似下面结果,说明运行时和 Skill 包路径都正常:

`{  “success”: true,  “status”: “ok”,  “skill_exists”: true}`
再发起一次真实查询:

`curl -N “http://127.0.0.1:8000/query/stream?question=北京明天适合户外跑步吗?”`
期望返回:

`event: startdata: {“success”:true,”message”:”agent query started”}event: progressdata: {“message”:”正在读取 Skill 并执行查询”}event: finaldata: {“success”:true,”answer”:”来源: Open-Meteon查询意图: 北京明天户外跑步天气查询n建议: 明天上午更适合跑步,注意风速和体感温度…”,”source”:”langgraph-agent”,”error”:null,”meta”:{“skill_path”:”…/skills/weather-query”,”elapsed_ms”:12345,”recursion_limit”:30}}event: donedata: {“success”:true}`
如果你想做自动化测试,可以跑:

`.venv/bin/python -m pytest -q`
测试的重点不只是接口有没有返回 200,而是要覆盖这几件事:

* `/healthz`能检查 Skill 包是否存在;
* `/query/stream`能拒绝空问题;
* Agent 会先读取`SKILL.md`;
* 只能读取 skill 目录内文件;
* 只能执行`scripts/`下的 Python 脚本;
* 脚本超时和报错能返回可理解的错误;
* SSE 事件至少覆盖`start`、`final`、`done`,异常时能返回`error`。

做到这里,这个 Skill 就不再只是本地 Agent 的能力,而是一个可以被系统集成的 API 服务。

* * *

## 九、总结

这篇文章我们把 Skill API 化完整走了一遍。

简单来说,核心就三点:

* **Skill API 化的目标**,是把个人 Agent 能力变成团队系统能力;
* **推荐的架构**,是 HTTP 服务 + LangGraph Agent Runtime + 完整 Skill 包;
* **关键边界**,是模型负责判断和编排,脚本负责确定性执行,服务负责权限、超时、错误和返回格式。

如果只是快速验证,可以在服务器上安装 Codex 这类 Agent 直接跑 Skill。

但如果要长期作为业务接口提供服务,更推荐把 Skill 放进一个清晰的 Agent Runtime 里,让它既保留 Skill 的灵活性,又具备后端服务应有的稳定边界。

这也是我理解的 Agent Skill 真正进入工程化阶段的一步:从“我本地能用”,走向“系统都能调用”。

企业落地新闻资讯智能营销

直播间的AI小助手如何辅助你的购买决策

2026-7-2 4:34:00

AI知识库企业落地新闻资讯

知识库,建完3个月变"僵尸库",问题不在技术在文化

2026-7-2 5:27:36

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
购物车
优惠劵
搜索