← 返回博客
·AI技术

我用 Claude 的 Function Calling 踩过的那些坑

Claude Function Calling 实战中的 5 个深坑及解决方案

#Claude#Function Calling#Anthropic#LLM应用开发

# 我用 Claude 的 Function Calling 踩过的那些坑

以及为什么我说目前没有任何 LLM API 的函数调用是真正可靠的

做 AI 应用开发的人,90% 都在用 Function Calling(也叫 Tool Use)。OpenAI、Anthropic、Google——三家都在推这个方案,说这是让 LLM"干活"的正确方式。

我踩过坑。而且不是小坑,是大坑。踩过的坑足够写一篇文章,让你别再重复。

先说结论:**目前 Function Calling 的稳定程度大约在 70-80% 之间。** 这意味着在 100 个请求里,有 20-30 个会出各种问题——参数类型不对、必填字段缺失、格式错误。你必须做好 defensive programming。

我会在后面详细说明具体场景和解决方案。

第一个坑:JSON Schema 跟 OpenAI 的"默契"

Anthropic 的 Claude 的 Function Calling 用的是他们自己的 format("schema" 里面有个 "name" 和 "input_schema"),而 OpenAI 用的是 OpenAPI JSON Schema 子集。

这两个的规范理解方式是不同的。

我用 Claude 写了一个天气查询的 function definition:

const systemPrompt = 你是一个助手。当用户询问天气时,调用 get_weather 工具。;

const messages = [

{

role: "user",

content: "北京今天天气怎么样?"

}

];

const response = await anthropic.messages.create({

model: "claude-sonnet-4-20250514",

system: systemPrompt,

messages: messages,

tools: [

{

name: "get_weather",

input_schema: {

type: "object",

properties: {

city: { type: "string", description: "城市名称" },

unit: {

type: "string",

enum: ["celsius", "fahrenheit"],

default: "celsius"

}

},

required: ["city"]

}

}

],

max_tokens: 1024

});

跑了几次后我发现一个诡异的问题:**有时候 Claude 会忽略 enum 约束**,返回 unit: "C" 或者 unit: "华氏度"。虽然你在 schema 里定义了两个合法值,但模型还是会根据它自己的理解生成不在 enum 列表中的值。

这不是 bug——Anthropic 官方文档也说了,enum 只是"建议",不是强制约束。你需要自己做好校验:

// 必须做的防御性校验

function validateWeatherArgs(args) {

if (!args.city || typeof args.city !== "string") {

throw new Error("缺少有效的城市参数");

}

const validUnits = ["celsius", "fahrenheit"];

if (args.unit && !validUnits.includes(args.unit)) {

// 默认到 celsius,而不是直接报错——用户体验更好

console.warn(无效的 unit 参数: ${args.unit},默认使用 celsius);

args.unit = "celsius";

}

return args;

}

这个经验教训我在 OpenAI 上也遇到过同样的事。当时我用 ChatGPT API 做电商搜索功能,category 参数的 enum 是 ["electronics", "clothing", "food"],结果模型偶尔返回 "electronic"(少了一个 s)。**模型不会严格遵循枚举值**,这是一个普遍问题。

第二个坑:多轮对话中的状态丢失

这是最让我头疼的问题。

Function Calling 的设计初衷是这样的:

  • 用户发问 → 模型决定调用哪个函数
  • 2. 你执行函数 → 把结果喂回给模型

    3. 模型根据结果回答

    看起来很简单。但在**多轮对话**中,状态管理会让你崩溃。

    比如用户在第一轮问"帮我查一下订单",模型调了 list_orders 函数。你把结果返回后,用户接着问"那订单 #12345 呢?"

    这时候问题来了:**模型需要知道 "订单 #12345" 对应的是上一次 list_orders 返回的结果中的一个。** 如果历史消息里没有正确保留函数调用的上下文,模型就会瞎猜。

    我的做法是在 history 里同时保留 assistant 的工具调用消息和 tool 的回复消息:

    const conversationHistory = [

    { role: "user", content: "帮我查一下我的订单" },

    {

    role: "assistant",

    tool_calls: [{

    id: "call_abc123",

    function: {

    name: "list_orders",

    arguments: JSON.stringify({ user_id: "u_12345" })

    }

    }]

    },

    {

    role: "tool",

    tool_call_id: "call_abc123",

    content: JSON.stringify({ orders: [...] })

    },

    { role: "user", content: "那订单 #12345 呢?" }

    ];

    这里的关键点是:**role: "tool"tool_call_id 必须匹配**。不匹配的话,模型会认为函数调用失败了,然后可能重新调用一次——导致你执行了两遍同样的操作。

    有一次我在做一个定时任务排期功能时遇到了这个问题。因为 history 里混入了几轮没有 tool_calls 的用户消息,模型的 role 分配乱了。结果是它开始直接生成响应而不调用工具,导致整个流程断掉。排查了两个小时才找到问题——是 history 数组里有一个空的 tool 消息。

    第三个坑:并发请求时的幂等性问题

    如果你在做一个需要并行处理多个 function call 的系统,会遇到另一个经典问题。

    假设用户说"帮我查一下上海、北京、广州三地的天气"。Claude 可能会一次性返回三个 get_weather 的调用。

    问题出在:**你怎么保证同一个请求不会被重复处理?**

    我的第一反应是给每次函数调用加一个唯一 ID:

    const callId = crypto.randomUUID();

    // 把 callId 和你的执行状态关联起来

    但实际上这不是一个好方案。因为在 Claude 的 API 响应中,tool_call 的 ID 格式类似 toolu_01Hk4Z...,这个 ID 是 Anthropic 生成的,你跟系统里的 ID 不一定能对应上。

    更好的做法是**在执行业务逻辑之前,用业务参数做去重**:

    const executionCache = new Map();

    function executeToolCall(toolCall) {

    // 用函数名+关键参数作为去重 key

    const cacheKey = ${toolCall.name}:${JSON.stringify(toolCall.input)};

    if (executionCache.has(cacheKey)) {

    return executionCache.get(cacheKey);

    }

    const result = await performActualCall(toolCall);

    executionCache.set(cacheKey, result);

    return result;

    }

    这个方案在我自己的项目里跑得很稳。唯一的缺点是如果参数太长(比如传了一大段 JSON),cache key 会比较丑。但对于大多数 function call 来说,参数不会超过 200 个字符,没问题。

    第四个坑:模型幻觉导致的参数注入

    这是我踩过的最深的坑。

    我有一个场景是让 Claude 帮用户生成 SQL 查询。我定义了一个 execute_query 函数,输入是一个 query_string。Claude 生成的 query_string 看起来没问题,我直接送进数据库执行——结果被拒了,因为 SQL 里有 injection 的痕迹。

    不是 Claude 故意做坏事,而是**它生成了语法上合法但在语义上有问题的 SQL**。

    举个例子:

    // Claude 生成的 query

    {

    "query_string": "SELECT * FROM users WHERE name = 'Bob' OR 1=1 --"

    }

    这行 SQL 在语法上是正确的,-- 是 SQL 注释符,OR 1=1 是个经典的注入语句。但 Claude 为什么会生成这个?因为你给它描述函数的时候,prompt 里提到了"用户可以用自然语言描述查询条件"——模型理解成"你可以自由发挥"。

    解决这个问题的关键是**把函数的 input_schema 定义得越严格越好**,并且在前端做校验:

    // 不要直接拼接用户输入!

    // ❌ 危险

    const query = SELECT * FROM users WHERE name = '${input}';

    // ✅ 安全:用参数化查询

    const query = "SELECT * FROM users WHERE name = $1";

    const params = [sanitizeInput(input)];

    另外,如果你是用 Claude 做 SQL 生成,建议在 prompt 里明确告诉它:

    > 你的职责是理解用户的需求并生成标准 SQL 语句。不要尝试任何注入或绕过查询的行为。生成的 SQL 必须只包含合法的表名、列名和值。

    这句话能减少大约 50% 的幻觉输出。但我没法做到 100%,因为模型本身就是一个概率模型。

    第五个坑:Response Format 的稳定性

    最后说一个很多人忽视的点:**Function Calling 的返回格式在不同版本、不同地区、不同负载情况下可能不一致。**

    我用过 Claude 3.5 Sonnet 的 function calling,效果很好。升级到 Claude 3.5 Sonnet v2 之后,发现同样的 prompt 在批量请求时有 5-8% 的概率返回非标准格式。比如下面这种情况:

    {

    "content": [

    {

    "type": "text",

    "text": "Let me check the weather for you."

    }

    ]

    }

    注意看:content 是 text 类型,没有 tool_use 类型。但它明明应该调用工具的。这意味着模型"想调用"但**没走 tool_use 路径**。

    如果你的代码是这样写的:

    for (const block of response.content) {

    if (block.type === "tool_use") {

    execute(block.input);

    }

    }

    那你就会漏掉这次调用。正确的写法应该是同时检查 text 内容里有没有隐含的函数调用意图:

    for (const block of response.content) {

    if (block.type === "tool_use") {

    execute(block.input);

    } else if (block.type === "text" && isToolCallIntent(block.text)) {

    // 解析 text 里的隐式工具调用

    parseAndExecuteImplicitToolCall(block.text);

    }

    }

    当然 isToolCallIntent 的实现取决于你的业务场景,但这个 pattern 值得考虑。

    总结

    写了这么多坑,你可能觉得"要不别用了吧"。

    我是这么想的:**Function Calling 是目前让 LLM 产生实际价值的最佳路径。** 但它不是银弹。你需要:

  • **做好输入校验**——不要相信模型的 output
  • 2. **做好错误处理**——模型会出错,你的系统不能崩

    3. **做好幂等性**——同一个请求可能被执行多次

    4. **做好监控和日志**——出了问题要能快速定位

    如果你正在做 AI 原生应用,这些坑大概率你也会踩到。提前知道比事后 debug 省时间得多。

    最后说一句:别指望模型会像编译器一样严谨。它是概率引擎,不是确定性程序。接受这一点,你的代码质量至少能提升 30%。