将 Fal.ai 的 any-llm 端点适配为 Claude Messages API 格式,通过 Prompt 工程为不支持原生工具调用的模型提供完整的 function calling 能力。
Fal.ai 的 any-llm 端点提供了统一的 LLM 访问接口,但存在以下限制:
- ❌ 仅支持 2 个参数:
prompt和system_prompt(纯文本) - ❌ 不支持原生 tools:无 function calling API
- ❌ 参数长度限制:标准端点每个参数最多 5000 字符(最近新增的企业端点无限制)
而 Claude Messages API 提供了:
- ✅ 结构化的 messages 数组
- ✅ 原生的 tools 定义和调用
- ✅ 多轮对话上下文管理
本项目的目标:在不修改上游 API 的前提下,通过 Prompt 工程实现完整的工具调用能力。
- ✅ Prompt 工程实现工具调用 - 为只有
prompt/system_prompt的上游提供 function calling - ✅ 堆栈截断防幻觉 - 确保工具调用后模型停止输出,等待真实结果
- ✅ 智能端点切换 - 根据 prompt 长度自动选择标准或企业端点
- ✅ 模型名称映射 - 灵活映射 Claude 模型名到实际 Fal 模型
- ✅ 完整 Claude API 兼容 - 流式/非流式、多轮对话、工具调用
由于上游只接受纯文本 system_prompt,我们将工具定义、调用规则、对话历史全部编码为 XML 结构:
输入侧(注入 system_prompt):
<system> 用户的原始 system prompt </system> <conversation_history> <user>之前的用户消息</user> <assistant>之前的助手回复</assistant> <tool_result call_id="xxx">工具执行结果</tool_result> </conversation_history> <tools> <tool name="get_weather"> <description>获取天气信息</description> <parameters> <json_schema>{"type":"object","properties":{...}}</json_schema> </parameters> </tool> </tools> <tool_call_rules> <rule>只能调用本轮 <tools> 中列出的工具</rule> <rule>输出 <tool_calls>...</tool_calls> 后立即停止,不得继续输出</rule> <rule>禁止模拟工具执行结果</rule> <rule>线性依赖:一次只调用一个工具,等待真实结果后再继续</rule> <rule>并行调用:无依赖时可在同一 <tool_calls> 中列出多个</rule> </tool_call_rules> <output_format> 仅回答内容(无需工具时) 或 <tool_calls> <tool_call name="..." call_id="..."> <arguments>{...JSON...}</arguments> </tool_call> </tool_calls> </output_format> 输出侧(模型响应):
可选的文本回复 <tool_calls> <tool_call name="get_weather" call_id="call_xxx"> <arguments>{"city": "北京"}</arguments> </tool_call> </tool_calls> 问题:模型可能在输出 </tool_calls> 后继续"幻觉",例如:
- 自行编造工具执行结果
- 继续模拟下一轮对话
- 输出额外的解释文字
解决方案:栈式 XML 解析器 + 第一闭合即停止
解析流程: 1. 遇到 <tool_calls> → 入栈 2. 遇到 <tool_call> → 入栈,记录 name 和 call_id 3. 遇到 <arguments> → 入栈,开始收集内容 4. 遇到 </arguments> → 出栈,解析 JSON 5. 遇到 </tool_call> → 出栈,保存工具调用 6. 遇到 </tool_calls> → 出栈,🔒 立即停止解析 关键点:第 6 步遇到第一个 </tool_calls> 闭合标签时,立即 break,后续内容全部丢弃。
效果:
- ✅ 模型无法在工具调用后继续输出
- ✅ 必须等待下一轮注入
<tool_result>才能继续 - ✅ 强制模型遵循"调用 → 等待 → 结果 → 继续"的流程
模型输出可能不规范,需要容错处理:
场景 1:忘记闭合 </arguments>
<tool_call name="get_weather"> <arguments>{"city": "北京"} </tool_call> ← 直接跳到这里了 处理:检测到 </tool_call> 时,如果栈顶还是 arguments,自动闭合并解析
场景 2:JSON 带尾部逗号
<arguments>{"city": "北京",}</arguments> 处理:正则替换 ,} → },,] → ]
场景 3:缺少 input
<tool_call name="get_weather" call_id="xxx"> </tool_call> 处理:自动补充空对象 {}
多轮对话需要将历史压缩到 system_prompt 中:
编码规则:
- 最后一条消息 → 放入
prompt参数(用户当前问题) - 其余历史 → 编码为 XML 放入
system_prompt
工具结果的处理:
用户: "北京天气" 助手: <tool_calls>...</tool_calls> ← 工具调用 用户: [tool_result: {"temp": 15}] ← 工具结果 编码为: <conversation_history> <user>北京天气</user> <assistant_tool_calls> <tool_call name="get_weather" call_id="xxx">...</tool_call> </assistant_tool_calls> <tool_result call_id="xxx">{"temp": 15}</tool_result> </conversation_history> 当前消息: (助手基于工具结果继续回答) Fal.ai 提供两个端点:
- 标准端点 (
fal-ai/any-llm):每个参数 ≤5000 字符 - 企业端点 (
fal-ai/any-llm/enterprise):最近新增,无长度限制,成本更高
选择逻辑:
if (system_prompt.length > 5000 || prompt.length > 5000) { 使用企业端点 } else { 使用标准端点 } 优化思路:
- 工具定义在每轮重复,占用较多空间
- 历史对话会累积增长
- 超过 5000 字符时自动切换,对用户透明
上游限制:只有非流式 API (fal.subscribe())
下游需求:客户端可能要求 SSE 流式输出
解决方案:
- 上游统一使用非流式调用,获取完整响应
- 解析完整内容,提取文本和工具调用
- 下游需要流式时,将内容分块输出:
- 文本按 15 字符/块输出,间隔 10ms(模拟打字)
- 工具调用的 JSON 按 20 字符/块输出,间隔 8ms
- 发送标准 SSE 事件:
message_start→content_block_delta→message_stop
优势:
- 简化实现,无需复杂的流式状态机
- 上游解析只需处理完整内容,无需处理截断
# 克隆项目 git clone https://github.com/your-username/fal2claude.git cd fal2claude # 启动服务 docker-compose up -d # 查看日志 docker-compose logs -f在 docker-compose.yml 中配置模型映射:
environment: - MODEL_MAPPING={"claude-sonnet-4-5-20250929":"anthropic/claude-3.7-sonnet"}curl http://localhost:8080/v1/messages \ -H "x-api-key: YOUR_FAL_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5-20250929", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 1024 }'curl http://localhost:8080/v1/messages \ -H "x-api-key: YOUR_FAL_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemini-2.5-flash", "messages": [{"role": "user", "content": "北京现在几点?"}], "tools": [{ "name": "get_current_time", "description": "获取指定城市的当前时间", "input_schema": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } }] }'响应:
{ "id": "msg_xxx", "type": "message", "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_xxx", "name": "get_current_time", "input": {"city": "北京"} } ], "stop_reason": "tool_use" }curl http://localhost:8080/v1/messages \ -H "x-api-key: YOUR_FAL_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemini-2.5-flash", "messages": [ {"role": "user", "content": "北京现在几点?"}, {"role": "assistant", "content": [ {"type": "tool_use", "id": "toolu_xxx", "name": "get_current_time", "input": {"city": "北京"}} ]}, {"role": "user", "content": [ {"type": "tool_result", "tool_use_id": "toolu_xxx", "content": "14:30"} ]} ], "tools": [...] }'请求转换 (Claude → Fal) ├─ 提取 messages、system、tools ├─ 构建 system_prompt │ ├─ 原始 system │ ├─ 对话历史 (XML) │ ├─ 工具定义 (XML) │ ├─ 调用规则 (XML) │ └─ 输出格式 (XML) └─ 提取最后一条消息 → prompt 上游调用 ├─ 选择端点 (标准/企业) ├─ fal.subscribe(endpoint, { │ prompt: "...", │ system_prompt: "...", │ model: "..." │ }) └─ 获取完整文本响应 响应解析 (Fal → Claude) ├─ 栈式 XML 解析 │ ├─ 识别 <tool_calls> │ ├─ 提取工具名、call_id、arguments │ └─ 🔒 遇到 </tool_calls> 立即停止 ├─ 分离文本和工具调用 └─ 构建 Claude API 响应 下游输出 ├─ 非流式: 直接返回 JSON └─ 流式: 分块输出 SSE 事件 | 变量 | 说明 | 默认值 |
|---|---|---|
PORT | 服务端口 | 8080 |
MODEL_MAPPING | 模型映射 JSON | {} |
注意:API Key 从请求头 x-api-key 或 Authorization: Bearer <key> 中提取。
[req-abc123] 📥 收到请求 [req-abc123] 请求模型: google/gemini-2.5-flash [req-abc123] Messages: 1 条 | Tools: 1 个 | Stream: 否 [req-abc123] 🔧 Prompt 转换完成 [req-abc123] system_prompt 长度: 487 字符 [req-abc123] message_prompt 长度: 18 字符 [req-abc123] 🎯 端点选择: fal-ai/any-llm [req-abc123] 原因: system=487, message=18 [req-abc123] 🚀 调用上游 fal.ai... [req-abc123] ✅ 上游返回成功 [req-abc123] 输出长度: 156 字符 [req-abc123] 🔍 解析结果 [req-abc123] 文本内容: 0 字符 [req-abc123] 工具调用: 1 个 [req-abc123] 工具调用详情: [{"id":"toolu_xxx","name":"get_current_time","input":{"city":"北京"}}] [req-abc123] 📤 返回非流式响应 [req-abc123] ✅ 完成 本项目展示了 Prompt 工程在 API 适配中的应用:
- 突破 API 限制:将只有 2 个文本参数的简单接口,扩展为支持结构化工具调用的复杂协议
- 防幻觉机制:通过堆栈截断确保模型严格遵循工具调用流程,不会自行编造结果
- 协议转换:在 Claude Messages API 和 Fal.ai 的文本接口之间建立双向映射
适用场景:
- 为不支持原生 function calling 的 LLM 提供工具调用能力
- 统一多个上游 API 为标准的 Claude 格式
- 在保持客户端兼容性的前提下切换后端模型
MIT
