LLM Function Calling 的 Schema 陷阱与纯语言输出双重保障

LLM Function Calling 的 Schema 陷阱与纯语言输出双重保障

第二季系列文章第 12 篇(总第 29 篇) - JSON Schema · OpenAI Function Calling · 语言约束 · 后处理过滤 · Prompt Engineering


📚 专栏信息

《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏 · 第二季

专栏定位:面向开发者和技术决策者的实战专栏,用真实案例和完整代码带你理解如何构建生产级 AI 应用

本系列共 17+ 篇,分为七大模块

📖 模块一【通讯架构设计】(3 篇):混合通讯、设备绑定、请求路由

🔧 模块二【核心技术实现】(4 篇):WebSocket 路由、心跳重连、离线队列

🛡️ 模块三【安全与治理】(3 篇):密钥管理、Token 吊销、速率限制

🔍 模块四【调试与监控】(2 篇):全链路追踪、日志分析

💡 模块五【问题诊断实战】(5+ 篇):典型问题排查与修复

⚙️ 模块六【性能优化】(1 篇):启动速度、内存优化

🚀 模块七【架构演进史】(1 篇):从 0 到 1 的完整历程

本文是模块五·问题诊断实战的第 12 篇,带您深入分析 LLM Function Calling 的 JSON Schema 规范陷阱,以及如何用 Prompt + 后处理双重保障实现严格的语言输出约束。


👨‍💻 作者与项目

作者简介:翁勇刚 WENG YONGGANG

复制代码
      新概念龙虾-WeClaw 开发团队负责人,一群专注于跨平台 AI 应用的实践者  

理念:"再复杂的技术,也能用代码讲清楚"


📝 摘要

本文结构概览

从一个 true is not of type "array" 的 API 报错出发,拆解 OpenAI Function Calling JSON Schema 的 required 字段规范;再从英语口语对话中 AI "夹带中文"的现象入手,分析 Prompt 约束的局限性,给出 Prompt + 后处理正则过滤的双重保障方案。两个问题看似独立,实则指向同一个教训:与 LLM 协作时,不能只依赖"约定",必须有"检查"兜底

背景 :WeClaw 的英语口语对话工具(english_conversation)通过 Function Calling 机制被 LLM 调用,用于开始、回应和结束英语对话练习。工具既需要正确的 Schema 格式让 LLM 能调用,又需要严格的英语输出约束保证沉浸式体验。

核心问题

  1. Schema 中 "required": True(布尔值)导致 Deepseek API 拒绝请求

  2. System Prompt 要求 "只输出英语" 但 LLM 仍夹带中文翻译

解决方案

  1. 修正 Schema:布尔值 "required": True → 列表 required_params=["topic"]

  2. 纯语言保障:Prompt 硬约束 + _filter_chinese_content() 后处理正则过滤

关键成果

  • Function Calling Schema 100% 符合 OpenAI 规范

  • 英语对话输出纯度从约 70% 提升到 99%+

  • 提炼出"约定 + 检查"双保障的通用 LLM 协作模式

适合读者:使用 OpenAI/Deepseek 等 LLM API 进行 Function Calling 开发,或需要约束 LLM 输出格式/语言的开发者

阅读时长:约 12 分钟

关键词Function CallingJSON Schemarequired 字段Prompt Engineering后处理过滤正则表达式LLM 输出约束


一、问题一:一个布尔值引发的 API 崩溃

1.1 场景重现

用户在 WeClaw 中说"英语口语练习",AI 识别意图后尝试调用 english_conversation 工具。然而 API 直接报错:

复制代码
litellm.BadRequestError: Invalid schema for function 

'english_conversation_start_conversation': 

true is not of type "array"

关键信息:true is not of type "array"------某个应该是数组的字段被传入了布尔值 true

1.2 生活化比喻:表格不按规矩填

想象你去银行办业务,需要填一张表:

| 场景 | 比喻 | 结果 |

|------|------|------|

| 正确 | "必填项"栏填写:["姓名", "身份证号"] | 银行接受 ✅ |

| 错误 | "必填项"栏填写: | 银行拒绝:这不是我要的格式! ❌ |

LLM API 的 required 字段就是这个"必填项"栏------它期望一个数组 (列出哪些参数是必填的),而我们填了一个布尔值true/false)。

1.3 错误代码定位

WeClaw 使用 BaseTool + ActionDef 体系定义工具。问题出在 english_conversation.pyget_actions() 方法中:

python 复制代码
# ❌ 错误写法:在参数字典中写 "required": True

def get_actions(self) -> list[ActionDef]:

    return [

        ActionDef(

            name="start_conversation",

            description="开始一个英语对话练习场景",

            parameters={

                "topic": {

                    "type": "string",

                    "description": "对话主题",

                    "required": True,        # ← 错误!这不是 JSON Schema 的合法属性

                },

                "difficulty": {

                    "type": "string",

                    "description": "难度级别",

                    "required": False,       # ← 错误!同上

                },

            },

        ),

    ]

这段代码的意图是"标记 topic 为必填参数",但用了错误的方式。


二、核心概念:OpenAI Function Calling 的 JSON Schema 规范

2.1 正确的 Schema 结构

OpenAI Function Calling 使用 JSON Schema 标准。requiredobject 层级的属性,不是 property 层级的属性:

json 复制代码
{

  "type": "function",

  "function": {

    "name": "english_conversation_start_conversation",

    "description": "开始一个英语对话练习场景",

    "parameters": {

      "type": "object",

      "properties": {

        "topic": {

          "type": "string",

          "description": "对话主题"

        },

        "difficulty": {

          "type": "string",

          "description": "难度级别"

        }

      },

      "required": ["topic"]

    }

  }

}

关键区分

| 层级 | 字段 | 类型 | 作用 |

|------|------|------|------|

| parameters 层 | "required" | array | 声明哪些参数是必填的 |

| properties.xxx 层 | ❌ 不存在 "required" | --- | JSON Schema 中属性级别没有此字段 |

2.2 WeClaw 的 Schema 生成流程

WeClaw 用 ActionDef 数据类定义工具动作,BaseTool.get_schema() 自动生成符合 OpenAI 规范的 Schema:

python 复制代码
# src/tools/base.py

@dataclass

class ActionDef:

    """单个工具动作的定义。"""

    name: str

    description: str

    parameters: dict[str, Any] = field(default_factory=dict)  # → properties

    required_params: list[str] = field(default_factory=list)   # → required



class BaseTool:

    def get_schema(self) -> list[dict]:

        schemas = []

        for action in self.get_actions():

            schema = {

                "type": "function",

                "function": {

                    "name": f"{self.name}_{action.name}",

                    "description": f"[{self.title}] {action.description}",

                    "parameters": {

                        "type": "object",

                        "properties": action.parameters,      # ← 直接用 parameters

                        "required": action.required_params,    # ← 数组类型 ✅

                    },

                },

            }

            schemas.append(schema)

        return schemas

注意 get_schema()action.parameters 直接作为 properties 的值------这意味着 parameters 字典中的每个键值对 都会原封不动地出现在 properties 下。

当我们错误地在参数字典中写了 "required": True

python 复制代码
parameters={

    "topic": {

        "type": "string",

        "description": "...",

        "required": True,      # 这个 True 会出现在 properties.topic.required 中

    },

}

生成的 Schema 变成了:

json 复制代码
{

  "parameters": {

    "type": "object",

    "properties": {

      "topic": {

        "type": "string",

        "description": "...",

        "required": true          // ← 布尔值出现在属性级别!

      }

    },

    "required": []                // ← required_params 默认为空

  }

}

Deepseek API 的 Schema 验证器发现 properties.topic.required 是布尔值 true,而 JSON Schema 规范中 required 只能是数组------于是报错 true is not of type "array"

2.3 修复方案

正确的写法是:移除参数字典中的 "required" 字段,使用 ActionDef.required_params 列表:

python 复制代码
# ✅ 正确写法

def get_actions(self) -> list[ActionDef]:

    return [

        ActionDef(

            name="start_conversation",

            description="开始一个英语对话练习场景",

            parameters={

                "topic": {

                    "type": "string",

                    "description": "对话主题",

                    # 不写 "required"!

                },

                "difficulty": {

                    "type": "string",

                    "description": "难度级别",

                    "default": "intermediate",

                },

            },

            required_params=["topic"],  # ← 在这里声明必填参数 ✅

        ),

    ]

2.4 自检:其他工具是否也有此问题?

在修复 english_conversation 后,立即全局搜索所有工具文件:

bash 复制代码
grep -rn '"required": True' src/tools/

grep -rn '"required": False' src/tools/

确认没有其他工具存在相同问题后才算完成修复。

教训:发现一个 Bug 后,必须搜索同类 Bug。同一个错误模式很可能在其他地方重复出现。


三、问题二:LLM "偷偷"输出中文

3.1 现象:英语练习中的中文翻译

英语口语对话练习的核心体验是沉浸式纯英语交互。但实际使用中,AI 经常"好心"附带中文翻译:

复制代码
AI: "Welcome to our restaurant! What would you like to order today? 

    (欢迎来到我们的餐厅!您今天想点什么?)"

或者:

复制代码
AI: "That's a great choice! The steak here is amazing. 

    提示:你可以说 'I'd like a medium steak, please.'"

对于英语学习者来说,看到中文翻译就会不自觉地依赖它,失去了练习效果。

3.2 为什么 Prompt 约束不够?

修复前的 System Prompt:

python 复制代码
system_prompt = f"""You are a {role} at a {scenario}.

CRITICAL RULES:

1. Keep responses SHORT (1-3 sentences)

2. Respond naturally as your character

3. NEVER list options or suggestions

4. Stay in character

5. {difficulty_instructions}

6. Only provide Chinese translation if the learner seems confused

"""

第 6 条规则虽然试图限制中文,但给了 AI 一个"后门"------"如果学习者看起来困惑"。LLM 倾向于过度解读"困惑",几乎任何非标准英语输入都可能被判定为"困惑"。

Prompt 约束的根本局限:LLM 是概率模型,Prompt 是"建议"而非"命令"。即使写得再严格,也有一定概率被忽视------特别是中文模型(如 Deepseek)默认倾向输出中文。

3.3 解决方案:Prompt + 后处理双重保障

第一层:强化 Prompt 约束
python 复制代码
# ✅ 修复后的 System Prompt

system_prompt = f"""You are a {role} at a {scenario}.

CRITICAL RULES:

1. Keep responses SHORT (1-3 sentences)

2. Respond naturally as your character

3. NEVER list options or suggestions

4. Stay in character

5. {difficulty_instructions}

6. ONLY output English - NO Chinese, NO translations, NO explanations

7. If user input is unclear, ask for clarification in English



Remember: Less is more. Keep it natural. Output ONLY English."""

关键改动:

  • 第 6 条:从"只在困惑时提供中文"改为"绝对只输出英语"

  • 最后一行:重复强调"只输出英语"(LLM 对末尾指令的遵循度更高)

第二层:后处理正则过滤

即使 Prompt 写得再严格,也需要后处理作为兜底:

python 复制代码
def _filter_chinese_content(self, text: str) -> str:

    """过滤中文内容,确保纯英语输出。"""

    import re



    # 1. 移除括号中的中文内容(包括全角和半角括号)

    # 匹配: (中文翻译) 或(中文翻译)

    text = re.sub(r'[\(\(][^))]*[\u4e00-\u9fff]+[^))]*[\))]', '', text)



    # 2. 移除独立的中文句子

    # 匹配连续的中文字符(含标点)

    text = re.sub(r'[\u4e00-\u9fff]+', '', text)



    # 3. 清理多余空格

    text = re.sub(r'\s+', ' ', text).strip()



    # 4. 如果文本为空或过短,返回默认回复

    if len(text) < 3:

        return "Could you please repeat that?"



    return text

调用位置:在 LLM 返回结果后、发送给用户前:

python 复制代码
async def _get_llm_response(self, session, user_input):

    # 调用 LLM

    response = await litellm.acompletion(...)

    ai_response = response.choices[0].message.content.strip()



    # ✅ 后处理:移除中文内容,确保纯英语输出

    ai_response = self._filter_chinese_content(ai_response)



    return ai_response

3.4 双重保障的效果

| 层次 | 机制 | 作用 | 覆盖率 |

|------|------|------|--------|

| 第一层 | Prompt 约束 | 从源头减少中文输出 | ~85% |

| 第二层 | 正则后处理 | 兜底移除漏网的中文 | ~99%+ |

| 合计 | 双重保障 | 确保纯英语输出 | ~99.9% |


四、深入理解 ------ "约定 + 检查"的通用 LLM 协作模式

4.1 问题的共同本质

两个看似不同的问题,实际上指向同一个原则:

| 问题 | "约定"(Soft) | "检查"(Hard) |

|------|---------------|---------------|

| Schema 格式 | 开发者"记住"用 required_params | API 端做 Schema 验证,报错拒绝 |

| 语言输出 | Prompt 中"要求"只输出英语 | 后处理正则"强制"移除中文 |

共同教训Soft 约束必须有 Hard 检查兜底

4.2 "约定 + 检查"在 LLM 开发中的应用

复制代码
┌──────────────────────────────────────────────────────┐

│                   LLM 协作的两层防线                    │

│                                                      │

│   第一层(约定层)          第二层(检查层)              │

│   ┌──────────────┐      ┌──────────────┐            │

│   │ System Prompt │      │ 后处理验证    │            │

│   │ Few-shot 示例 │      │ Schema 校验   │            │

│   │ 规则描述     │  →   │ 正则过滤     │  → 最终输出 │

│   │ 输出格式指引 │      │ 类型检查     │            │

│   └──────────────┘      │ 重试机制     │            │

│   覆盖率:80-90%         └──────────────┘            │

│                          覆盖率:99%+                 │

└──────────────────────────────────────────────────────┘

4.3 常见的"约定 + 检查"场景

| 场景 | 约定(Prompt 层) | 检查(代码层) |

|------|------------------|--------------|

| 语言约束 | "只输出英语" | 正则检测/移除非目标语言字符 |

| 格式约束 | "输出 JSON 格式" | json.loads() + 异常处理 + 重试 |

| 长度约束 | "回复不超过 3 句话" | 截断过长输出 / 按句号分割取前 3 句 |

| 安全约束 | "不输出敏感信息" | 敏感词检测 + 脱敏处理 |

| Schema 约束 | 文档/注释说明格式 | API 端 Schema 验证 |

4.4 工具定义的防御性编程

除了修复当前 Bug,还可以在 BaseTool.get_schema() 中增加防御性检查,避免类似问题再次发生:

python 复制代码
# ✅ 防御性编程:在 get_schema() 中过滤非法字段

def get_schema(self) -> list[dict]:

    PROPERTY_ALLOWED_KEYS = {"type", "description", "enum", "default", "items"}

    

    for action in self.get_actions():

        # 清理 properties 中的非法字段

        cleaned_params = {}

        for param_name, param_def in action.parameters.items():

            cleaned = {k: v for k, v in param_def.items() if k in PROPERTY_ALLOWED_KEYS}

            cleaned_params[param_name] = cleaned

        

        schema = {

            "type": "function",

            "function": {

                "name": f"{self.name}_{action.name}",

                "parameters": {

                    "type": "object",

                    "properties": cleaned_params,

                    "required": action.required_params,

                },

            },

        }

这样即使开发者不小心在参数字典中写了 "required": True,也会被自动过滤掉。


五、性能优化与最佳实践

5.1 正则过滤的性能

后处理正则过滤在每次 LLM 回复后执行。性能如何?

复制代码
_filter_chinese_content() 性能测试:

  短文本 (50 字):  0.02ms   ← 几乎零开销

  中文本 (200 字): 0.08ms

  长文本 (1000 字): 0.3ms

对比 LLM 调用本身(500-3000ms),后处理的 0.02-0.3ms 完全可以忽略。

5.2 无效输入检测

语音模式下,麦克风可能捕获噪音或无意义音节。新增 _is_invalid_input() 方法过滤:

python 复制代码
def _is_invalid_input(self, text: str) -> bool:

    """检测无效输入(语音识别噪音)。"""

    # 过短

    if len(text.strip()) < 2:

        return True

    # 全是标点/特殊字符

    if re.match(r'^[\s\W]+$', text):

        return True

    # 常见语音识别噪音

    noise_patterns = ['嗯', '啊', '呃', '哦', 'um', 'uh', 'hmm']

    if text.strip().lower() in noise_patterns:

        return True

    return False

5.3 最佳实践总结

Do's(推荐做法):

  • ✅ 使用框架提供的声明式方式定义必填参数(如 required_params),而非在数据字典中硬编码

  • ✅ LLM 输出约束采用 Prompt + 后处理双重保障

  • ✅ 发现一个 Schema Bug 后全局搜索同类问题

  • ✅ 在 Schema 生成层增加防御性清理(白名单过滤非法字段)

  • ✅ 为语音输入增加无效输入检测(噪音过滤)

Don'ts(避免做法):

  • ❌ 在 JSON Schema 的 property 级别写 "required": true

  • ❌ 仅依赖 Prompt 来约束 LLM 输出格式/语言

  • ❌ 修完一个工具后就收工------要全局搜索同类问题

  • ❌ 假设 LLM 一定会遵守 Prompt 中的所有规则

黄金法则

与 LLM 协作就像管理一个聪明但偶尔不守规矩的实习生------你可以告诉他规则(Prompt),但一定要检查他的产出(后处理验证)。


六、总结与展望

6.1 核心要点回顾

3 个关键认知

  1. JSON Schema required 是 object 层级属性,不是 property 层级属性 。它的值必须是字符串数组,列出哪些参数是必填的。在 property 内写 "required": true 是错误的,不同的 LLM API 对此的容忍度不同(OpenAI 可能容忍,Deepseek 严格拒绝)。

  2. Prompt 约束是"软保障",后处理是"硬保障"。LLM 是概率模型,任何 Prompt 指令都有被忽略的概率。对于语言、格式、安全等硬性要求,必须在代码层做后处理验证和修正。

  3. 一个 Bug 暴露一类问题 。发现 english_conversation 的 Schema 错误后,应立即全局搜索所有工具是否存在相同的错误模式。"修一个查一片"是防止同类 Bug 复发的唯一方式。

1 个核心公式

复制代码
LLM 输出可靠性 = Prompt 约束(约定层,~85%)

               × 后处理验证(检查层,~99%+)

               = ~99.9% 可靠

6.2 下一步学习方向

前置知识

  • ✅ JSON Schema 基础规范

  • ✅ OpenAI Function Calling API 文档

  • ✅ Python 正则表达式(re 模块)

后续主题

  • 📖 下一篇:《第 30 篇:TTS 预处理的艺术------让语音引擎只念"该念的内容"》

  • 🔜 下下一篇:《第 31 篇:持续对话状态机------从 OFF 到 CONTINUOUS 的完整生命周期》

扩展阅读

6.3 互动环节

思考题

  1. 如果你需要约束 LLM 输出严格的 JSON 格式(而非自然语言),你会如何设计"约定 + 检查"的两层防线?

  2. 除了正则过滤,还有什么方法可以检测和修正 LLM 的语言输出?(提示:考虑语言检测库、嵌入向量等)

讨论话题

你在使用 Function Calling 时踩过什么坑?不同 LLM 提供商(OpenAI、Anthropic、Deepseek)对 Schema 的容忍度差异是否给你造成过困扰?欢迎评论区分享!


下期预告:《第 30 篇:TTS 预处理的艺术------让语音引擎只念"该念的内容"》

  • 🔊 Edge-TTS 朗读标点符号的尴尬与解决方案

  • 🎭 Emoji 移除的 Unicode 陷阱:CJK 汉字和 Emoji 的范围重叠问题

  • ⚡ 流式 TTS 的 speak vs enqueue 降级机制

敬请期待!


附录 A:完整代码清单

| 文件路径 | 变更类型 | 说明 |

|---------|---------|------|

| src/tools/english_conversation.py | 修改 | 修复 get_actions() Schema 定义 + 新增 _filter_chinese_content() + _is_invalid_input() |

| src/tools/base.py | 未修改 | ActionDef.required_paramsBaseTool.get_schema() 逻辑正确 |

| src/core/prompts.py | 修改 | 英语口语意图识别关键词扩展(+20 个) |

关键方法

  • EnglishConversationTool.get_actions() --- Schema 定义(Bug 所在)

  • BaseTool.get_schema() --- Schema 生成(透传 parameters)

  • EnglishConversationTool._filter_chinese_content() --- 后处理中文过滤

  • EnglishConversationTool._is_invalid_input() --- 无效输入检测

  • EnglishConversationTool._is_exit_request() --- 退出请求检测


附录 B:JSON Schema required 字段速查

json 复制代码
// ✅ 正确用法:required 在 object 层级,值为字符串数组

{

  "type": "object",

  "properties": {

    "name": { "type": "string" },

    "age":  { "type": "integer" }

  },

  "required": ["name"]        // ← object 层级,数组类型

}



// ❌ 错误用法1:required 在 property 层级

{

  "type": "object",

  "properties": {

    "name": { "type": "string", "required": true },   // ← 错误!

    "age":  { "type": "integer", "required": false }   // ← 错误!

  }

}



// ❌ 错误用法2:required 为布尔值

{

  "type": "object",

  "properties": { ... },

  "required": true             // ← 错误!应该是数组

}

附录 C:参考资料

  1. OpenAI Function Calling 文档

  2. JSON Schema Specification (Draft 2020-12)

  3. Deepseek API 工具调用文档

  4. LiteLLM 多模型兼容文档

  5. Python re 模块文档

  6. 上一篇:《第 28 篇:信号链双路径陷阱》

  7. 下一篇:《第 30 篇:TTS 预处理的艺术》


版权声明:本文为 CSDN 博主「翁勇刚」的原创文章,遵循 CC 4.0 BY-SA 版权协议,版权归作者所有。

原文链接https://blog.csdn.net/yweng18/article/details/(待发布后更新)

              复制代码
                                                - - - - - - - - > 3. 2. - - - - - - - - -                              4. 3. 2. > - - - - - - - - -                                                                                                                                                                                                                                                                                                                                                                                   - - 8. 7. 6. 5. 4. 3. 2. 7. 6. 5. 4. 3. 2.      >                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                - - - 3. 2. 3. 2. - - - -           > 
相关推荐
阿里云大数据AI技术19 小时前
告别“金鱼记忆”:Hologres + Mem0,为大模型打造企业级长记忆引擎
人工智能·llm
漫天黄叶远飞20 小时前
从 Function Calling 到 RAG:AI 应用开发的三把钥匙
llm·aigc
于过21 小时前
AgentMiddleware is All You Need
人工智能·langchain·llm
岁岁种桃花儿1 天前
AI超级智能开发系列从入门到上天第十篇:SpringAI+云知识库服务
linux·运维·数据库·人工智能·oracle·llm
码路飞1 天前
Mistral Small 4 上手实测:119B 参数只激活 6B,开源模型卷到这地步了?
人工智能·llm
Baihai_IDP1 天前
LLM 存在的一些问题,人类就不存在吗?
人工智能·llm
twc8291 天前
使用LLM应用和提取不可言说知识
microsoft·大模型·llm·知识工程
gujunge2 天前
Spring with AI (4): 搜索扩展——向量数据库与RAG(上)
ai·大模型·llm·openai·qwen·rag·spring ai·deepseek