中文 | English
基于 Claude Agent Skills 设计理念实现的 OpenAI 格式 API 服务,支持 Skill 元工具。
- ✨ Skill 元工具:LLM 自主决定何时调用技能
- 📁 自动发现:扫描
skills/目录加载.md格式的技能文件 - 🔄 两种注入策略:
- message 系统提示词替换(
{{SKILL}}占位符) - system 消息注入(可清除/切换)
- message 系统提示词替换(
- 🔌 OpenAI 兼容:标准
/v1/chat/completions接口
openai-skill-proxy/
├── src/ # 源代码目录
│ ├── __init__.py
│ ├── main.py # FastAPI 主服务入口
│ ├── config.py # 配置管理(环境变量)
│ ├── models.py # Pydantic 数据模型
│ ├── skill_loader.py # Skill 加载器(扫描 & 解析 .md 文件)
│ ├── skill_marker.py # Skill 标记处理(提取/附加/移除)
│ ├── openai_client.py # OpenAI API 客户端
│ ├── strategy_system.py # 策略1: 系统提示词替换
│ └── strategy_message.py # 策略2: 消息注入
│
├── skills/ # 技能定义目录
│ ├── 发票.md # 发票相关问答知识库
│ ├── 融资.md # 融资相关问答知识库
│ ├── translator.md # 翻译助手技能
│ ├── summarizer.md # 摘要专家技能
│ └── code-review.md # 代码审查技能
│
├── prompts/ # 提示词模板目录
│ └── system_prompt.md # 多轮对话系统提示词
│
├── tests/ # 测试脚本目录
│ ├── test_skill.py # Skill 加载测试(交互式)
│ ├── run_test.py # 非交互式测试
│ └── chat_test.py # 多轮对话测试
│
├── .env # 环境变量配置
├── requirements.txt # Python 依赖
└── README.md # 项目说明
┌─────────────────────────────────────────────────────────────────────┐
│ 用户请求 │
│ POST /v1/chat/completions │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ FastAPI 服务 │
│ (main.py) │
├─────────────────────────────────────────────────────────────────────┤
│ 1. 解析请求参数 │
│ 2. 注入 Skill 工具定义到 tools 列表 │
│ 3. 调用上游 LLM API │
└─────────────────────────────────────────────────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ LLM 未调用 Skill │ │ LLM 调用 Skill │
│ 直接返回响应 │ │ function_call │
└──────────────────┘ └──────────────────┘
│
▼
┌───────────────────────────────┐
│ 处理 Skill 工具调用 │
│ handle_skill_tool_call() │
├───────────────────────────────┤
│ 1. 解析 skill 名称 │
│ 2. 从 skill_loader 获取内容 │
│ 3. 应用注入策略 │
└───────────────────────────────┘
│
┌────────────────────┴────────────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 策略1: system │ │ 策略2: message │
│ 替换 {{SKILL}} │ │ 注入 user 消息 │
│ 占位符 │ │ <skill> 标签包裹 │
└──────────────────────┘ └──────────────────────┘
│ │
└────────────────────┬────────────────────┘
▼
┌───────────────────────────────┐
│ 第二次调用 LLM API │
│ (带 skill 上下文) │
└───────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ 返回最终响应 │
└───────────────────────────────┘
用户: "银行融资怎么操作?"
│
▼
┌─────────────────────────────────────┐
│ 第一次 LLM 调用 │
│ (包含 Skill 工具定义) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ LLM 返回 tool_calls: │
│ { │
│ "name": "Skill", │
│ "arguments": {"command": "融资"} │
│ } │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 加载 skills/融资.md 内容 │
│ 注入到消息列表 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 第二次 LLM 调用 │
│ (带知识库上下文) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ LLM 基于知识库回答: │
│ "工商银行融资操作流程如下..." │
└─────────────────────────────────────┘
pip install -r requirements.txtcp .env.example .env
# 编辑 .env 填写 API 配置.env 配置项:
OPENAI_API_KEY=your-api-key
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4
SKILL_STRATEGY=message # system 或 message
SKILLS_DIR=skills
SKILL_SECOND_CALL_THINKING=false # 二次请求是否开启思考| 配置项 | 说明 |
|---|---|
SKILL_STRATEGY |
技能注入策略:system(替换占位符) 或 message(消息注入) |
SKILL_SECOND_CALL_THINKING |
附加 Skill 后的二次请求是否开启思考模式:true=保持, false=关闭 |
关于二次思考:当检测到 Skill 调用时,系统会注入技能内容并发起二次请求。
设置为false可以避免二次请求再次执行思考过程,加快响应速度。
python -m src.main服务将在 http://localhost:8000 启动。
python tests/chat_test.pyPOST /v1/chat/completions请求示例:
{
"model": "gpt-4",
"messages": [{ "role": "user", "content": "帮我审查这段代码: print(hello)" }],
"strategy": "message"
}当 LLM 判断需要使用技能时,会自动调用 Skill 工具,系统将注入对应技能的提示词。
| 参数 | 类型 | 说明 |
|---|---|---|
strategy |
system | message |
注入策略(默认从env环境变量读取) |
include_skill_tool |
boolean |
是否包含 Skill 工具(默认启用,设置 false 关闭) |
stream |
boolean |
是否启用流式输出(默认 false) |
top_p |
float |
采样参数(直接透传上游) |
temperature |
float |
温度参数(直接透传上游) |
max_tokens |
integer |
最大 token 数(直接透传上游) |
支持 SSE 流式输出,设置 stream: true 即可:
{
"model": "gpt-4",
"messages": [{ "role": "user", "content": "你好" }],
"stream": true
}流式 Skill 调用处理:
当流式回复中检测到 Skill 工具调用时:
- 已输出的内容(思考/文本)继续转发,不断开连接
- 系统注入 Skill 内容后发起二次请求
- 二次请求的响应追加到当前流中
用户请求 → 流式输出(思考内容)... → 检测到Skill调用 → 注入Skill → 二次请求 → 追加输出回复 → [DONE]
如果首个工具调用不是 Skill,则直接透传所有内容,不做拦截。
GET /v1/skills/list # 列出所有技能
GET /v1/skills/reload # 重新加载技能
POST /v1/skills/clear # 清除消息中的技能注入在 skills/ 目录创建 .md 文件:
---
name: my-skill
description: 技能描述,用于 LLM 决策是否调用
---
你是一个专业的 XXX...
## 工作流程
...Skill 文件格式要求:
- 必须包含 YAML frontmatter
name: 技能名称(用于 LLM 调用)description: 技能描述(LLM 根据此判断何时调用)- 正文内容:技能的知识库或指令
| 策略 | 触发条件 | 消息链结构 | 特点 |
|---|---|---|---|
| system | system 消息含 {{SKILL}} |
替换占位符 + tool_calls 链 | 适合固定模板,自动清除历史 |
| message | 默认策略 | tool_calls 链 + user 消息 | 灵活,支持清除/切换 |
在 system 消息中使用 {{SKILL}} 占位符,技能内容会替换此占位符:
请求示例:
{
"messages": [
{
"role": "system",
"content": "你是助手。\n\n{{SKILL}}<--提示词必须要包含Skill占位符,用于后续替换。"
},
{ "role": "user", "content": "工行融资怎么操作?" }
],
"strategy": "system"
}消息链流程:
原始消息:
├── system: "你是助手。\n\n{{SKILL}}"
└── user: "工行融资怎么操作?"
│
▼ (第一次 LLM 调用,返回 tool_calls)
│
应用 skill 后:
├── system: "你是助手。\n\n[SKILL_CONTENT][融资.md 内容...][/SKILL_CONTENT]" ← 用标记包裹
├── user: "工行融资怎么操作?"
├── assistant: { tool_calls: [{ name: "Skill", arguments: { command: "融资" } }] }
└── tool: "The '融资' skill has been loaded into system prompt."
│
▼ (第二次 LLM 调用)
│
最终回复: "工商银行融资操作流程如下..."
多轮对话处理:
在 system 模式下进行多轮对话时,如果用户切换到不同的技能话题,系统会:
- 自动清除历史消息中之前的 Skill 工具调用记录(
assistant的tool_calls和对应的tool消息) - 还原占位符:将
[SKILL_CONTENT]旧内容[/SKILL_CONTENT]还原为{{SKILL}} - 替换新技能:用新技能内容替换占位符
多轮对话示例:
├── system: "你是助手。\n\n[SKILL_CONTENT][融资.md 内容...][/SKILL_CONTENT]"
├── user: "工行融资怎么操作?"
├── assistant: { tool_calls: [...] } ← 第一次 Skill 调用(会被清除)
├── tool: "..." ← 对应的 tool 消息(会被清除)
├── assistant: "融资操作流程..."
├── user: "发票问题怎么处理?"
│
▼ (用户问了新话题,LLM 决定调用发票技能后,messages 修改为)
│
处理后:
├── system: "你是助手。\n\n[SKILL_CONTENT][发票.md 内容...][/SKILL_CONTENT]" ← 替换为新技能
├── user: "工行融资怎么操作?"
├── assistant: "融资操作流程..."
├── user: "发票问题怎么处理?"
├── assistant: { tool_calls: [{ name: "Skill", arguments: { command: "发票" } }] }
└── tool: "The '发票' skill has been loaded into system prompt."
技能内容作为新的 user 消息注入,支持清除和切换:
请求示例:
{
"messages": [{ "role": "user", "content": "工行融资怎么操作?" }],
"strategy": "message"
}消息链流程:
原始消息:
└── user: "工行融资怎么操作?"
│
▼ (第一次 LLM 调用,返回 tool_calls)
│
应用 skill 后:
├── user: "工行融资怎么操作?"
├── assistant: { tool_calls: [{ name: "Skill", arguments: { command: "融资" } }] }
├── tool: "The '融资' skill is loading"
└── user: "[SKILL_INJECTION:融资]\n[融资.md 内容...]\n[/SKILL_INJECTION]\n请根据上述技能知识库的内容回答用户的问题。"
│
▼ (第二次 LLM 调用)
│
最终回复: "工商银行融资操作流程如下..."
清除已注入的技能:
POST /v1/skills/clear
{"messages": [...]}为了让标准 OpenAI 客户端(不处理 _skill_context)也能在多轮对话中保持 Skill 上下文,服务端会在首次加载 Skill 时在回复末尾附加一个不可见的标记:
<!-- @skill:技能名称 -->【首次触发 Skill】
用户: "融资怎么操作?"
↓
LLM 调用 Skill("融资") → 注入知识库 → 生成回复
↓
服务端返回assistant: "融资操作流程如下...<!-- @skill:融资 -->"
↓
客户端保存 assistant 消息(包含标记)
【后续追问】
用户: "手续费多少?"
↓
客户端发送 messages(包含带标记的历史 assistant 消息)
↓
服务端检测到 <!-- @skill:融资 --> 标记
↓
自动恢复 Skill 上下文(重建 tool_call 链 + 注入知识库)
↓
LLM 直接回答(无需再次调用 Skill 工具)
↓
返回回复(不再附加新标记)
当服务端检测到历史消息中有 Skill 标记时,会自动:
- 重建 tool_call 链:生成随机 ID,插入完整的
assistant(tool_calls)+tool(result)消息 - 注入 Skill 内容:根据策略注入到 system prompt 或 messages
- 插入位置:在首次带标记的 assistant 消息之前
- 清理标记:调用 LLM 前移除所有 assistant 消息中的 marker(LLM 不应看到这些标记)
| 场景 | 是否附加标记 |
|---|---|
| 首次调用 Skill | ✅ 附加 |
| 追问(预加载模式) | ❌ 不附加 |
| 切换到新 Skill | ✅ 附加新标记 |
| 无 Skill 相关对话 | ❌ 不附加 |
注意:标记仅用于前端与服务端之间传递上下文,不会发送给 LLM 模型。
| 脚本 | 说明 |
|---|---|
tests/api_test.py |
多轮对话测试(真实 API) |
tests/stream_test.py |
流式对话测试(真实 API) |
tests/stream_chat_test.py |
流式多轮对话(验证 Skill 标记) |
tests/test_marker.py |
Skill 标记模块单元测试 |
调用Skill工具只能调用一个无法并列调用。
MIT