跳到主要内容

76 篇博文 含有标签「LLM」

大语言模型相关

查看所有标签

AI 更擅长写 React 还是 Vue?被榜单掩盖的真相

· 阅读需 12 分钟

先从一个反直觉的数字说起。

如果你去看 2025 年的 JavaScript Rising Stars,React 以 24.2 万 GitHub stars 一骑绝尘,Vue 只有 5.26 万,看起来是近五倍的碾压级差距。但如果你记得几年前的榜单,Vue 和 React 的 stars 曾长期咬得很紧,远不是今天这个差距。

发生了什么?Vue 凉了吗?并没有。真相是 Vue 3 把仓库拆分成了 vuejs/core,stars 从零重新计数,而已归档的旧仓库 vuejs/vue(20.9 万 stars)不再计入新口径。同一个框架,换个统计口径,就从「并驾齐驱」变成了「不到 React 的四分之一」。

这个口径陷阱,是理解「AI 更擅长写 React 还是 Vue」这个问题的绝佳入口——因为我们对这个问题的几乎所有「共识」,都建立在类似的口径陷阱之上。


一、表面的共识:人人都说 AI 更会写 React​

只要你在用 AI 辅助前端开发,大概率有过这种体感:让 Claude 或 GPT 写 React,一次通过率高、代码像模像样;换成 Vue,就容易出现新旧 API 混用、模板结构出错的情况。

这个体感并非空穴来风。生态体量的差距是实打实的:

  • npm 周下载量:React 约为 Vue 的 4~10 倍以上(口径波动很大,含 CI、镜像和 React Native 的污染);
  • Stack Overflow Developer Survey 2025:React 使用率 44.7%,Vue 17.6%,相差约 2.5 倍;
  • GitHub 使用仓库数:行业估计 React 约为 Vue 的 5~10 倍。

尤雨溪 2022 年在知乎的回答里指出过,React 的 npm 下载量有相当一部分来自 React Native,按 Chrome DevTools 扩展的周活估算,纯 Web 端的真实使用比大约是 1.5~2:1,远没有 npm 数字那么夸张。

但请注意一个关键转折:LLM 的训练语料来自公开代码,而不是真实使用分布。npm 数字或许高估了「人的差距」,但公开仓库和代码语料里 React 的压倒性占比,恰恰就是 AI 看到的世界。

到这里为止,故事似乎很顺:React 语料多 → AI 写 React 更强。问题在于:这个「更强」几乎从未被严肃地测量过。

二、被榜单掩盖的事实:主流榜单根本没测过 Vue​

开发者社区判断「AI 写前端谁强」,最常用的依据是 LMArena 的 WebDev Arena 榜单。但翻看 WebDev Arena 的官方说明(2025-06-13)会发现一个惊人的细节:它的系统提示词写死了「你是一名专家级前端 React 工程师……生成 React 组件……使用 TypeScript 和 Tailwind」。

换句话说,WebDev Arena 是一个 React-only 榜单。它只能告诉你哪个模型在 React 栈上相对更强,对「React vs Vue」这个问题零发言权。同理,Aider Polyglot 只到语言粒度,Design2Code 以 HTML 为目标,都不含框架维度。

真正系统对比过 React / Vue / Angular / vanilla 的学术基准,目前只有两个。

DesignBench:头部模型 React 与 Vue 几乎打平​

DesignBench(2025-06 首发,v3 更新于 2026-03)用 900 个网页样本测试了 9 个多模态大模型,结果相当微妙:

模型React 编译成功率(CSR)Vue 编译成功率(CSR)
Claude-3.70.95410.9746(全场最佳)
GPT-4o0.97250.9492

不仅编译成功率互有胜负,在部分质量指标上 Vue 还反超了:Claude-3.7 的设计生成 CLIP 相似度 Vue 0.8319 > React 0.8083,设计编辑 MLLM 评分 Vue 8.36 > React 8.18。

论文另外两个结论同样值得记住:所有模型在框架代码上都弱于 vanilla HTML/CSS——框架特有语法对所有人都是瓶颈;两者的错误模式还不一样,React 主要栽在 JSX 解析类错误(Unexpected Token),Vue 主要栽在模板结构类错误(Missing End Tag)。Angular 则以 0.69~0.76 的编译成功率明显掉队。

Web-Bench:多数模型 React 领先,但有反超​

字节的 Web-Bench(2025-05)让四个框架共享同一套任务,Pass@2 对比如下:

模型ReactVueAngularSvelte
Claude-3.765304025
Claude-3.7-Thinking被反超反超 React—反超 React

多数模型确实是 React 最高,论文也归因于 React 语料最多,以及「JSX 把 JavaScript 和 HTML 结合,提升了数据密度」。但必须把反面也摆出来:Claude-3.7-Thinking 在 Vue 和 Svelte 上反超了 React,论文原文明确写道 Vue/Svelte「与 HTML 语法相似,展现出更优的性能」。

两个基准合在一起,结论很清楚:**差距存在,但远比社区想象的小,而且在部分指标、部分模型上互有胜负。**至于中文社区流传的那组「Claude 写 React 一次通过率远超 Vue」的精确百分比,它出自 CSDN 内容农场,无方法、无样本量,没有任何引用价值,本文不予采信。

三、真正的差距在哪里​

既然框架能力差距没那么大,「AI 写 React 更强」的体感从何而来?答案藏在三层结构性因素里。

1. 语料体量:JSX 排第 8,Vue 排第 16​

最硬的证据来自 Replit 公开的 replit-code-v1-3b 训练语料配比(基于 The Stack):按 token 降序,JSX 作为一门单独的「语言」排第 8,比 Rust、C、Go、C++ 都多;而 Vue 只排第 16,还在 HTML 之后。

React 代码在训练语料里是「头等公民」,Vue 是「二等公民」。这不是框架优劣问题,是纯粹的数据分布问题。侧面证据是 State of JS 2024:Vue 的留存率 87% 反而高于 React 的 75%。用过 Vue 的人满意度不差,只是写 Vue 的公开代码少。

2. 英文语料主导:庞大的中文 Vue 社区近乎隐形​

Vue 的用户大量集中在中国和亚洲,而 CommonCrawl 语料约 46% 是英文、中文只占 3% 左右,GPT 系语料的中文占比同样很低(<0.1%~7%)。掘金、知乎、中文博客上浩如烟海的 Vue 教程、踩坑记录、最佳实践,在模型训练时几乎不存在。Vue 社区的地理分布,恰好撞上了 AI 语料的语言盲区。

3. 语料新旧混杂:同一个问题有三代「正确答案」​

两个框架都有范式变迁带来的语料污染。React 经历了 class 组件(含已废弃生命周期)→ hooks(2019)→ Server Components(2022+)三代范式,arXiv:2509.20277 已证实 LLM 会生成引用已废弃组件的代码。Vue 这边,Vue 2 于 2023-12-31 正式 EOL,而 Options API 与 Composition API 官方承诺长期双轨并存,语料里 Vue 2 Options、Vue 2.7、Vue 3 <script setup> 三种风格混杂。有开发者实测观察(社区经验,非严格基准)发现 AI 生成 Vue 代码时常见新旧 API 混用。

另外需要标注的是,社区里流行的「Vue 三段式 SFC 模板/脚本/样式逻辑分散、对 AI 不友好,React 纯 JS 单上下文更利于生成」的说法(源自掘金等社区文章的对比表格),是社区观点而非实测结论——DesignBench 和 Web-Bench 的数据恰恰提示,类 HTML 的模板语法对模型也有独特优势。

4. 组件库格局:碎片化是 React 的隐藏成本​

还有一个少有人提的反向变量:React 组件库是头部多极格局——MUI(约 9.5 万 stars)、Ant Design(约 9.4 万)、shadcn/ui(约 8.3 万)、Chakra(约 3.9 万)……LLM 需要记忆多套互不兼容的 API,生成的代码常常张冠李戴。Vue 则向 Element Plus 和 Vuetify 双头集中,模型「押对」的概率反而更高。有意思的是,shadcn/ui「把组件源码复制进项目」的模式意外地为 React 解了围——LLM 从「凭记忆猜 API」变成「读本地源码」,绕开了碎片化问题。这也提示 Vue 用户:让 AI 读项目里的真实代码,比让它背框架 API 可靠得多。

四、工具链的制度性锁定与马太效应​

比语料更要命的是工具链。语料差距是历史存量,工具链的差距是每天都在发生的新增量:

  • v0 是架构级 React-only:泄露的系统提示词硬性规定 React + Next.js App Router + Tailwind + shadcn/ui,第三方评测直言「用 Vue 的话 v0 完全没用」;
  • Lovable 固定 React + TypeScript + Vite + Tailwind + Supabase,根本没有 Vue 选项;头部工具里只有 Bolt.new 支持 Vue,且复杂构建的质量不稳;
  • 前端 AI 工具横评(PinkLime,2026-02)显示 Vue/Nuxt 的支持「全面降一档」,Claude Code 在 .vue 文件里偶尔还会默认套 React 的模式;
  • 连学术界都默认 React:字节的 Flame 论文构建的是 React 生成基准,并自认「扩展到 Vue/Angular 是未来工作」。

这就形成了一个正反馈循环:语料多 → 生成质量好 → 工具默认 React → 新项目更多选择 React → 产生更多语料。arXiv:2509.23261《The Matthew Effect of AI Programming Assistants》用 13.5 万次真实生成请求加上 17 个任务 × 6 个技术栈的严格实验量化了这个效应:主流技术栈 1~3 次尝试就能完成任务,小众栈尝试 5 次以上仍然失败——论文把这个现象称为「AI 生产力税」(AI Productivity Tax):选择小众栈,就要额外支付反复调试、人工兜底的成本。

但这篇论文里有个极易被忽略的关键细节:在实验中,Vue + Spring Boot 属于「主流栈」,表现良好。也就是说,Vue 的 AI 劣势高度集中在「纯前端生成工具」这个场景;在企业全栈场景里,Vue 根本不交「生产力税」。

五、Vue 阵营的补课:文档层与协议层​

面对锁定,Vue 阵营的回应值得一提:2025 年 5 月,尤雨溪宣布 Vue、Vite、Rolldown 官方文档全面接入 llms.txt(通过 vitepress-plugin-llms),让 LLM 能直接抓取结构化、无噪音的官方文档;社区也补位了 vite-plugin-vue-mcp、shadcn-vue-mcp 等 MCP 工具,让 AI Agent 能实时查询 Vue 组件和 API。

注意这个补课的形态:是「文档层 + 协议层」的建设,而不是「做一个 Vue 版 v0」。这种不对称本身很说明问题——生态的弱势方选择把自己变成对 AI 更友好的数据源,而不是正面复制对方的工具链。这条路能不能走通,还要观察。

六、给开发者的实际建议​

综合以上证据,我的建议分三个场景:

**已经在用 Vue 的团队:不必换。**学术基准显示头部模型在 Vue 上的能力差距远小于体感差距,提示词质量和工程实践的权重更高。务实的做法是:项目里维护一份 .cursorrules / CLAUDE.md,明确锁定 Vue 3 + <script setup> + Composition API,杜绝新旧 API 混用;接入 llms.txt 文档源或 Vue 相关 MCP;把常用组件用法沉淀成项目内示例。这些措施能抹平大部分体感差距。

**重度依赖 AI 生成工具链的新项目:React 生态确实更省力。**如果你的工作流深度绑定 v0、Lovable 这类「描述需求 → 生成完整页面」的工具,那这不是框架之争,是工具链之争——这些工具根本不给 Vue 选项。这时候选 React 是对工具链现实的妥协,而不是对框架优劣的投票。

**企业 Java 全栈场景(Vue + Spring Boot):按团队基因选,不用管 AI 焦虑。**马太效应论文自己的实验里这个组合就是「主流栈」,表现良好。AI 生产力税在这里不适用,让团队熟悉度和人才储备做决定即可。


最后回到开头的 stars 陷阱。「AI 更擅长 React」这个说法,很大程度上和「Vue stars 只有 React 的一个零头」是同一种错觉——统计口径、语料分布、工具链默认值共同制造的错觉。真实的情况是:AI 写 React 的平均下限确实更高,但那是数据分布和制度性锁定的产物;Vue 的 HTML 式模板本身并不吃亏,吃亏的是语料体量、中英文语料失衡和专用工具的缺位。

看清这一层的意义在于:框架选型应该基于团队、生态和业务,而不是基于一个被榜单放大的错觉。毕竟,AI 的「偏好」是会随着语料和工具变化的,而你的代码库要陪你很多年。

Function Calling 实践指南:角色、字段与报文处理全解析

· 阅读需 16 分钟

这是 Function Calling 系列的第二篇。第一篇《Function Calling 诞生的背景》聊了「为什么」需要 Function Calling,本文聚焦「怎么做」——从对话角色、字段定义到报文处理,手把手带你掌握 Function Calling 的实践细节。

前置知识:Function Calling 的基本流程​

在深入细节之前,先快速过一遍 Function Calling 的标准流程:

1. 定义函数列表(Schema)
└→ 告诉 LLM 有哪些函数可用、每个函数需要什么参数

2. 构造对话上下文
└→ 包含 system、user、assistant、tool 四种角色的消息

3. 调用 LLM API
└→ 传入 functions 定义 + 对话历史

4. 解析 LLM 返回
└→ LLM 返回 tool_calls(要调用的函数 + 参数)

5. 执行实际函数
└→ 你的代码执行函数,获得真实结果

6. 将结果反馈给 LLM
└→ 用 tool 角色将函数结果注入回对话

7. 重复步骤 3-6
└→ 直到 LLM 不再需要调用函数,给出最终回答

这个流程中有两个关键点需要深入理解:

  • 对话中涉及的角色体系:system、user、assistant、tool 的职责边界
  • 函数定义的字段规范:完整了解每个字段的作用和约束

下面逐个拆解。


一、对话中的四种角色​

Function Calling 的对话不是简单的「用户问 → 模型答」,而是涉及四种角色的复杂协作。每种角色有明确的职责边界,理解这一点才能正确构造对话上下文。

1. System 角色——全局规则制定者​

System 消息是整个对话的「宪法」,它定义了:

  • 模型的行为边界:什么能做、什么不能做
  • 领域知识:特定领域的专业术语、业务规则
  • 响应风格:语气、语言、格式偏好
  • 安全策略:敏感操作的前置检查

典型 System Prompt 结构:

system_prompt = """
你是一个专业的电商客服助手,负责帮助用户查询订单、处理退换货、查询库存。

你的职责:
- 准确理解用户意图,必要时调用相应函数获取真实数据
- 不要编造订单号、物流信息等事实性数据
- 对于无法满足的需求,明确说明原因并提供替代方案

安全规则:
- 涉及订单修改、退款等操作时,必须先确认用户身份
- 不要直接调用需要高权限的函数,先请求用户确认

响应风格:
- 用中文回答,语气友好专业
- 对于数据类查询,用表格形式呈现
- 对于操作类任务,列出步骤并询问是否确认执行
"""

System 角色的关键约束:

  • System 消息只能在对话开始时发送一次(OpenAI API 限制)
  • 它对整个对话生效,不能中途修改
  • 权重最高——如果 user 和 system 矛盾,模型倾向于遵守 system

2. User 角色——自然语言输入者​

User 消息是用户(或你的应用)的原始输入,特点是:

  • 自然语言表达:可以是口语化、模糊、不完整
  • 隐含信息丰富:用户可能不会显式说出所有参数
  • 多轮对话支持:可以通过追问补充信息

User 消息的几种典型形态:

# 形态 1:直接明确
{"role": "user", "content": "帮我查订单 20241201001 的物流状态"}

# 形态 2:模糊但可推导
{"role": "user", "content": "看看我昨天买的手机发货了吗"}
# → 需要从历史上下文推导订单号

# 形态 3:多参数隐含
{"role": "user", "content": "我要从北京飞上海,明天早上出发"}
# → destination="上海", origin="北京", date=明天日期
# → time_range="morning"(隐含约束)

处理 User 消息时的注意事项:

  • 不要过度解读:超出常识范围的「推导」容易出错
  • 善用历史:之前的对话可能包含关键信息(订单号、偏好等)
  • 必要时追问:当关键信息缺失时,让模型主动询问用户

3. Assistant 角色——决策中枢​

Assistant 消息有两种形态,职责完全不同:

形态 A:纯文本响应

这是传统的「模型回答」,用于:

  • 直接回答不需要函数调用的知识性问题
  • 确认用户意图,澄清歧义
  • 解释函数调用的结果
{
"role": "assistant",
"content": "好的,我帮您查询订单 20241201001 的物流状态。"
}

形态 B:函数调用决策(tool_calls)

这是 Function Calling 的核心——模型决定调用哪些函数,并提取参数:

{
"role": "assistant",
"content": None, # 注意:tool_calls 模式下 content 为 None
"tool_calls": [
{
"id": "call_abc123", # 调用 ID,用于后续关联结果
"type": "function",
"function": {
"name": "get_order_status",
"arguments": '{"order_id": "20241201001"}'
}
}
]
}

Assistant 决策的智能之处:

  1. 多函数同时调用(并行)

    {
    "tool_calls": [
    {"function": {"name": "get_order_status", ...}},
    {"function": {"name": "get_user_profile", ...}}
    ]
    }
  2. 参数隐式提取

    • 用户说「昨天」→ 模型计算出具体日期
    • 用户说「便宜」→ 模型根据上下文推断价格范围
  3. 空值智能处理

    • 可选参数未提及 → 自动留空,不传 null 或默认值
    • 必填参数缺失 → 决定先询问用户,而不是调用函数
  4. 错误避免

    • 发现参数明显不合理(如日期在 1900 年)→ 决定不调用,先确认

4. Tool 角色——函数结果的反馈者​

Tool 消息是你把函数执行结果「喂回」给模型的唯一渠道。它有三个关键属性:

属性 1:必须关联 tool_call_id

{
"role": "tool",
"tool_call_id": "call_abc123", # 必须匹配 Assistant 的 tool_calls[].id
"content": '{"status": "shipped", "tracking_number": "SF1234567890", "estimated_delivery": "2024-12-05"}'
}

为什么必须关联?

  • 允许一次 Assistant 回复调用多个函数
  • 模型需要知道哪个结果对应哪个调用
  • 多轮对话场景下,避免混淆

属性 2:内容要「模型可读」

Tool 的 content 不是给用户看的,是给模型继续推理用的。设计原则:

  • 结构化优于非结构化:用 JSON 而不是自然语言
  • 关键信息不能省略:不要假设模型「应该知道」
  • 错误信息要详细:失败时给出具体原因,方便模型调整策略
# ❌ 错误示例
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "订单不存在" # 太模糊,模型不知道为什么
}

# ✅ 正确示例
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": '{"error": "ORDER_NOT_FOUND", "message": "订单号 20241201001 不存在或已被删除", "suggestions": ["检查订单号是否输入正确", "联系客服确认"]}'
}

属性 3:可以注入多个结果

当一个 Assistant 调用了多个函数时,Tool 消息可以批量返回:

# Assistant 调用了两个函数:get_order_status 和 get_user_profile
# 你按顺序返回两个 Tool 消息

messages.append({
"role": "tool",
"tool_call_id": "call_abc123",
"content": '{"order_id": "20241201001", "status": "shipped", ...}'
})

messages.append({
"role": "tool",
"tool_call_id": "call_def456",
"content": '{"user_id": "u12345", "name": "张三", "level": "VIP", ...}'
})

二、函数定义的完整字段列表​

OpenAI 的 Function Definition 基于 JSON Schema,但不是所有字段都常用。以下是完整列表 + 实用建议。

函数级字段(function 对象外层)​

字段类型必填说明实用建议
namestring✅函数名称,用于标识用 snake_case(如 get_user_order),避免与 JS 内置方法冲突
descriptionstring✅函数功能描述清晰描述做什么,不要只写函数名;多描述场景而非实现细节
parametersobject✅参数的 JSON Schema必须设置 type: "object",即使没有参数
strictboolean❌是否严格输出(OpenAI 新增)设为 true 可以减少格式错误,但限制模型灵活性,生产环境推荐
additionalPropertiesboolean❌是否允许额外参数设为 false 可以防止模型传多余字段,提高解析稳定性

完整示例:

function_def = {
"name": "search_products",
"description": "根据关键词、分类、价格范围搜索商品。支持模糊匹配和排序。",
"strict": False,
"parameters": {
"type": "object",
"properties": {
# 详见下文
},
"additionalProperties": False
}
}

参数级字段(properties 对象内层)​

基础类型字段​

字段类型说明示例
typestring参数类型:string / number / integer / boolean / array / object"type": "string"
descriptionstring参数说明,模型用它理解如何提取"description": "商品关键词,支持模糊搜索"

字符串特有字段​

字段类型说明示例
enumarray[string]允许的枚举值["order_id", "tracking_number", "phone"]
formatstring字符串格式约束(非 JSON Schema 标准,但部分模型支持)"format": "date"
{
"search_type": {
"type": "string",
"description": "搜索方式:按订单号、运单号或手机号",
"enum": ["order_id", "tracking_number", "phone"]
}
}

数字特有字段​

字段类型说明示例
minimumnumber最小值(含)"minimum": 0
maximumnumber最大值(含)"maximum": 1000000
exclusiveMinimumboolean / number最小值(不含)或具体值"exclusiveMinimum": 0
exclusiveMaximumboolean / number最大值(不含)或具体值"exclusiveMaximum": 999999
{
"price_min": {
"type": "number",
"description": "价格下限(元),不填表示无下限",
"minimum": 0,
"exclusiveMaximum": 1000000
}
}

数组特有字段​

字段类型说明示例
itemsobject数组元素的类型定义见下方示例
minItemsinteger最小元素数量"minItems": 1
maxItemsinteger最大元素数量"maxItems": 10
uniqueItemsboolean元素是否必须唯一"uniqueItems": true
{
"category_ids": {
"type": "array",
"description": "商品分类 ID 列表,最多 10 个",
"items": {
"type": "integer",
"description": "分类 ID"
},
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
}
}

对象特有字段​

字段类型说明示例
propertiesobject对象的属性定义见下方示例
requiredarray[string]必填字段列表["name", "age"]
{
"address": {
"type": "object",
"description": "收货地址",
"properties": {
"province": {"type": "string", "description": "省份"},
"city": {"type": "string", "description": "城市"},
"district": {"type": "string", "description": "区县"},
"detail": {"type": "string", "description": "详细地址"}
},
"required": ["province", "city", "detail"]
}
}

参数级通用字段​

字段类型说明实用建议
requiredarray[string]必填参数列表只把真正必填的放进去,过多会降低模型调用成功率
defaultany默认值(当参数未提供时)慎用:Function Calling 中 default 常被忽略,更推荐在代码里处理
examplesarray[any]参数示例值对复杂类型(object / array)非常有效,帮助模型理解结构
# examples 的强大之处
{
"filter": {
"type": "object",
"description": "搜索过滤条件",
"properties": {
"brand": {"type": "string"},
"price_range": {
"type": "object",
"properties": {
"min": {"type": "number"},
"max": {"type": "number"}
}
},
"in_stock": {"type": "boolean"}
},
"examples": [
{"brand": "Apple", "price_range": {"min": 5000, "max": 10000}},
{"in_stock": true}
]
}
}

完整函数定义示例​

functions = [
{
"name": "search_products",
"description": "搜索商品,支持关键词、分类、价格、品牌等多维度筛选,返回商品列表和分页信息。",
"strict": False,
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "搜索关键词,支持商品名称、型号的模糊匹配。为空则返回所有符合条件的商品。"
},
"category_id": {
"type": "integer",
"description": "商品分类 ID,1-手机数码,2-家用电器,3-服装鞋帽,4-食品生鲜,5-图书文具"
},
"price_range": {
"type": "object",
"description": "价格范围约束",
"properties": {
"min": {
"type": "number",
"description": "最低价格(元),必须 ≥ 0",
"minimum": 0
},
"max": {
"type": "number",
"description": "最高价格(元)",
"minimum": 0
}
},
"examples": [
{"min": 0, "max": 100},
{"min": 1000, "max": 5000}
]
},
"brand": {
"type": "string",
"description": "品牌名称,精确匹配。如:Apple、华为、小米"
},
"in_stock": {
"type": "boolean",
"description": "是否只显示有货商品。true-仅显示有货,false-显示全部(包含无货)"
},
"sort_by": {
"type": "string",
"description": "排序方式",
"enum": ["relevance", "price_asc", "price_desc", "sales_desc", "rating_desc"]
},
"page": {
"type": "integer",
"description": "页码,从 1 开始",
"minimum": 1,
"default": 1
},
"page_size": {
"type": "integer",
"description": "每页数量,建议 10-50",
"minimum": 1,
"maximum": 100,
"default": 20
}
},
"required": [],
"additionalProperties": False
}
}
]

三、LLM 返回报文的处理流程​

LLM 的返回是 Function Calling 最容易出问题的地方。需要处理三种情况:文本响应、单次函数调用、多次函数调用。

情况 1:纯文本响应(不调用函数)​

# LLM 返回
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "好的,请问您要搜索哪个品牌的手机?预算大概是多少?"
},
"finish_reason": "stop"
}
]
}

处理逻辑:

def handle_text_response(response):
assistant_msg = response["choices"][0]["message"]
content = assistant_msg["content"]

# 直接返回给用户
return content

关键点:

  • finish_reason: "stop" 表示对话结束
  • content 不为 None,且没有 tool_calls 字段
  • 这是最简单的场景,直接转发即可

情况 2:单次函数调用​

# LLM 返回
{
"id": "chatcmpl-abc123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_xxx123",
"type": "function",
"function": {
"name": "search_products",
"arguments": '{"keyword": "iPhone", "price_range": {"min": 5000, "max": 10000}}'
}
}
]
},
"finish_reason": "tool_calls"
}
]
}

处理逻辑:

import json

def handle_function_call(response, functions_registry):
assistant_msg = response["choices"][0]["message"]
tool_calls = assistant_msg["tool_calls"]

# 保存 Assistant 消息到对话历史
messages.append(assistant_msg)

# 执行每个函数调用
tool_responses = []
for tool_call in tool_calls:
call_id = tool_call["id"]
function_name = tool_call["function"]["name"]
arguments_str = tool_call["function"]["arguments"]

# 1. 解析参数(可能失败)
try:
arguments = json.loads(arguments_str)
except json.JSONDecodeError as e:
# 参数解析失败,返回错误信息给模型
tool_responses.append({
"role": "tool",
"tool_call_id": call_id,
"content": json.dumps({
"error": "INVALID_ARGUMENTS_JSON",
"message": f"参数 JSON 解析失败: {str(e)}",
"raw_arguments": arguments_str
})
})
continue

# 2. 执行函数(可能失败)
try:
function_impl = functions_registry[function_name]
result = function_impl(**arguments)
# 将结果转为 JSON 字符串
result_str = json.dumps(result) if not isinstance(result, str) else result
except KeyError:
# 函数不存在
tool_responses.append({
"role": "tool",
"tool_call_id": call_id,
"content": json.dumps({
"error": "FUNCTION_NOT_FOUND",
"message": f"函数 {function_name} 不存在",
"available_functions": list(functions_registry.keys())
})
})
continue
except Exception as e:
# 函数执行失败
tool_responses.append({
"role": "tool",
"tool_call_id": call_id,
"content": json.dumps({
"error": "FUNCTION_EXECUTION_ERROR",
"message": str(e),
"function": function_name,
"arguments": arguments
})
})
continue

# 3. 成功执行,返回结果
tool_responses.append({
"role": "tool",
"tool_call_id": call_id,
"content": result_str
})

# 4. 将 Tool 结果加入对话历史,继续下一轮
messages.extend(tool_responses)
return messages # 传回给 LLM API 继续对话

关键点:

  • finish_reason: "tool_calls" 表示需要执行函数
  • 必须将 Assistant 消息保存到历史(即使 content 为 None)
  • 每个函数调用都生成一个 Tool 响应,通过 tool_call_id 关联
  • 错误处理要完善:JSON 解析错误、函数不存在、执行失败都要处理

情况 3:多次函数调用(并行)​

LLM 可能一次性决定调用多个函数,例如用户问「查一下订单 123 的状态,顺便看看我的账户余额」。

# LLM 返回
{
"choices": [
{
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_aaa",
"function": {"name": "get_order_status", "arguments": '{"order_id": "123"}'}
},
{
"id": "call_bbb",
"function": {"name": "get_account_balance", "arguments": '{"user_id": "user_456"}'}
}
]
},
"finish_reason": "tool_calls"
}
]
}

处理逻辑:

def handle_multiple_function_calls(response, functions_registry):
assistant_msg = response["choices"][0]["message"]
tool_calls = assistant_msg["tool_calls"]

# 保存 Assistant 消息
messages.append(assistant_msg)

# 并行执行所有函数(如果函数之间无依赖)
# 如果有依赖关系,按顺序执行
tool_responses = []

for tool_call in tool_calls:
call_id = tool_call["id"]
function_name = tool_call["function"]["name"]
arguments = json.loads(tool_call["function"]["arguments"])

try:
result = functions_registry[function_name](**arguments)
result_str = json.dumps(result) if not isinstance(result, str) else result
except Exception as e:
result_str = json.dumps({"error": str(e)})

tool_responses.append({
"role": "tool",
"tool_call_id": call_id,
"content": result_str
})

# 按 tool_call_id 排序(保持与 tool_calls 顺序一致)
tool_responses.sort(key=lambda x: x["tool_call_id"])

messages.extend(tool_responses)
return messages

关键点:

  • 多个函数调用可以并行执行(如果无依赖)
  • Tool 响应的顺序最好与 tool_calls 保持一致
  • 即使某个函数失败,其他函数的结果仍要返回

完整处理流程(伪代码)​

def chat_with_functions(user_message, conversation_history, functions_registry):
# 1. 构造当前对话上下文
messages = conversation_history + [{"role": "user", "content": user_message}]

# 2. 调用 LLM API
response = openai.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=[{"type": "function", "function": func_def} for func_def in functions_registry.values()],
tool_choice="auto" # 让模型自动决定是否调用函数
)

assistant_msg = response.choices[0].message

# 3. 判断响应类型
if assistant_msg.tool_calls:
# === 函数调用场景 ===
conversation_history.append(assistant_msg) # 保存 Assistant

tool_responses = []
for tool_call in assistant_msg.tool_calls:
# 执行函数并收集结果
result = execute_function(tool_call, functions_registry)
tool_responses.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})

conversation_history.extend(tool_responses)

# 递归调用,继续对话
return chat_with_functions(None, conversation_history, functions_registry)

else:
# === 纯文本响应场景 ===
conversation_history.append(assistant_msg)
return assistant_msg.content # 返回给用户

def execute_function(tool_call, functions_registry):
func_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)

func = functions_registry[func_name]
return func(**arguments)

四、常见问题与最佳实践​

问题 1:模型返回的参数类型不符合 JSON Schema​

现象:

// 定义时要求 integer,模型返回了 "123"(字符串)
{"user_id": "123"} // 应该是 123

原因:

  • 模型有时会「过度泛化」,将数字用字符串表示
  • 特别是 phone、order_id 这种「看起来像字符串的数字」

解决方案:

def validate_and_cast(value, schema):
if schema["type"] == "integer" and isinstance(value, str):
try:
return int(value)
except ValueError:
pass
# 其他类型转换...
return value

arguments = {k: validate_and_cast(v, param_schema) for k, v in arguments.items()}

问题 2:模型调用不存在的函数​

现象: 模型调用了一个你根本没定义的函数,如 get_user_info_v2。

原因:

  • 函数描述不够清晰
  • 模型认为你的函数「不合适」,自己「想」了一个更好的

解决方案:

  • 在 System Prompt 中明确:「只能调用提供的函数,不能编造新函数」
  • 确保函数定义覆盖所有场景
  • 在 Tool 响应中返回错误并告诉模型可用函数列表

问题 3:函数执行失败后模型陷入循环​

现象: 模型反复调用同一个函数,每次都失败。

原因:

  • 错误信息不够详细,模型不知道怎么调整
  • 模型认为「再试一次可能成功」

解决方案:

# 在错误信息中给出明确建议
{
"error": "INVALID_ORDER_ID",
"message": "订单号格式错误,应为 10 位数字",
"user_input": order_id,
"suggestions": [
"请检查订单号是否正确",
"订单号示例:20241201001"
]
}

# 在 System Prompt 中增加:
# "如果同一个函数连续失败 3 次,停止调用并告知用户无法完成"

问题 4:Context 过长导致 Token 溢出​

现象: 对话多次后,context_length_exceeded 错误。

原因:

  • 函数调用的结果太长
  • 历史对话积累太多

解决方案:

策略 1:压缩函数结果

# ❌ 返回完整数据
{
"products": [/* 100 个商品 */]
}

# ✅ 返回摘要
{
"total": 100,
"page": 1,
"products": [/* 前 3 个 */],
"note": "共 100 个商品,仅展示前 3 个,如需更多请翻页"
}

策略 2:智能裁剪历史

def trim_conversation(messages, max_tokens=4000):
# 保留 System
# 保留最近 5 轮对话
# 保留所有 Tool 结果(因为可能被引用)
# 删除早期的 Assistant / User 对话
pass

最佳实践总结​

场景建议
函数命名使用动词开头:get_xxx, search_xxx, create_xxx, delete_xxx
参数命名使用 snake_case,语义清晰:user_id 而不是 uid
描述质量描述「做什么」而非「怎么实现」;举例子比讲原理更有效
错误处理返回结构化错误,包含错误码、消息、建议、原始输入
结果格式尽量用 JSON,避免自然语言;大数据要摘要
历史管理定期压缩;保留最近轮次 + 关键 Tool 结果
测试覆盖测试所有分支:文本响应、单次调用、多次调用、错误场景

总结​

Function Calling 不是「让模型输出 JSON」这么简单,它涉及:

  1. 四种角色的协作:system 制定规则,user 输入意图,assistant 决策调用,tool 反馈结果
  2. 完整的字段体系:从函数级的 name / description / strict,到参数级的 type / enum / minimum / items / examples
  3. 健壮的报文处理:区分三种响应类型,处理 JSON 解析、函数执行、错误反馈、历史管理

理解了这些细节,你才能在生产环境中构建稳定可靠的 Function Calling 系统。

下一篇我们会聊进阶话题:Function Calling 的高级模式——链式调用、并行执行、条件分支、错误恢复。


延伸阅读:

扩展 LLM 的能力边界:联网搜索、文件读写与记忆功能全景指南

· 阅读需 10 分钟

引言​

上一篇我们聊了 Function Calling 诞生的背景——LLM 为什么必须学会调用函数。这篇更进一步,把问题摊开:

一个只会输出文本的模型,如何长出"眼睛"、"手"和"记忆"?

  • 联网搜索给它眼睛,让它看到训练数据截止之后的世界;
  • 文件读写给它手,让它能在真实工作环境中留下成果;
  • 记忆功能让它跨越会话,记住你是谁、上次做到哪一步。

这三项能力,加上代码执行和 MCP 协议的标准化,构成了今天所有 AI Agent 的地基。本文逐项拆解它们的原理、实现路径和工程上的坑。

Function Calling 诞生的背景:从 LLM 聊天到工具调用

· 阅读需 7 分钟

引言​

2023 年 6 月 13 日,OpenAI 发布 GPT-4-0613 和 GPT-3.5-turbo-0613,首次正式引入 Function Calling 能力。当时社区的反应两极分化:一线开发者欢呼雀跃,但也有人不以为然地评价:「不就是让模型输出 JSON 吗?」

两年半过去,答案已经明朗。Function Calling 是 LLM 从「聊天机器人」走向「AI Agent」最关键的一步基础设施创新。本文不教你怎么调 API,而是从工程视角系统梳理:Function Calling 到底解决了什么问题?它是怎么从社区 hack 演变为行业标准的?

用 LangChain + Qdrant + DeepSeek 搭建简历 RAG 系统

· 阅读需 4 分钟

背景​

需要从一份 Word 简历中提取结构化信息(姓名、专业技能、工作年限),用 RAG 方案比直接写正则解析灵活得多。

技术栈​

组件选型
文档加载UnstructuredLoader
向量库Qdrant (Docker)
嵌入模型通义千问 text-embedding-v1
LLMDeepSeek v4 Flash
编排框架LangChain
运行环境Python 3.13 + uv

项目结构​

rag/
├── main.py # 主流程
├── llm.py # LLM + 向量库封装
├── prompt.py # 本地 prompt 模板
├── sensenova.py # 备用 LLM 客户端
├── pyproject.toml # 依赖管理
└── uv.lock

核心流程​

1. 文档加载与切片​

word = UnstructuredLoader("简历.docx")
docs = word.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
chunks = splitter.split_documents(docs)

UnstructuredLoader 支持 .docx/.pdf/.pptx 等格式,自动提取文本。分片用 50 字符 + 20 字符重叠,对简历这种短文档粒度合适。

2. 向量化存储​

embeddings = DashScopeEmbeddings(
dashscope_api_key=os.environ.get("dashscope"),
model="text-embedding-v1",
)
vectordb = QdrantVectorStore.from_documents(
chunks, embeddings,
collection_name="resume",
url="http://localhost:6333",
)

嵌入用通义千问的 text-embedding-v1,向量库用 Qdrant(Docker 运行),collection 名 resume。

3. LCEL 链​

chain = {
"context": vec_store.as_retriever() | format_docs,
"question": RunnablePassthrough(),
} | prompt | llm | StrOutputParser()

用 LangChain Expression Language 串联:检索 → 格式化上下文 → 填充 prompt → LLM 推理 → 解析输出。

4. 查询​

chain.invoke("请输入姓名. 格式如下\n姓名: ?")
chain.invoke("总结专业技能情况. 格式如下\n专业技能: ?")
chain.invoke("根据工作年份总结工作经验. 格式如下\n工作经验: ?年")

三次调用分别提取姓名、技能、工作年限,prompt 里约束了输出格式。

实际输出​

姓名: XX
专业技能: Go, Python, C/C++, C#, Java, Lua
工作经验: 约16年

踩坑记录​

hub.pull 安全限制​

hub.pull("rlm/rag-prompt") 默认不允许拉取公开 prompt,需要:

from langsmith import Client as LangSmithClient
client = LangSmithClient()
prompt = client.pull_prompt("rlm/rag-prompt", dangerously_pull_public_prompt=True)

缺失依赖​

langchain-text-splitters、langchain-community、dashscope 需要在 pyproject.toml 中显式声明。

Qdrant 连接​

Qdrant 通过 Docker 启动,确保 localhost:6333 可访问。

完整代码​

main.py​

import os

from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_unstructured import UnstructuredLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langsmith import Client as LangSmithClient

from llm import DeepSeek, QdrantVecStoreFromDocs


def clearstr(s: str) -> str:
filter_chars = ['\n', '\r', '\t', '\u3000', ' ']
for char in filter_chars:
s = s.replace(char, '')
return s


def format_docs(docs) -> str:
return "\n\n".join(clearstr(doc.page_content) for doc in docs)


def load_doc():
word = UnstructuredLoader("C:\\Users\\Administrator\\Downloads\\xxx.docx")
docs = word.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
chunks = splitter.split_documents(docs)
vec_store = QdrantVecStoreFromDocs(chunks, collection_name="resume")
llm = DeepSeek()
client = LangSmithClient()
prompt = client.pull_prompt("rlm/rag-prompt", dangerously_pull_public_prompt=True)

chain = {
"context": vec_store.as_retriever() | format_docs,
"question": RunnablePassthrough(),
} | prompt | llm | StrOutputParser()

ret = chain.invoke("请输入姓名. 格式如下\n姓名: ?")
print(ret)
ret = chain.invoke("总结专业技能情况,内容可能包含golang、AI Agent、python、rag等.格式如下\n专业技能: ?")
print(ret)
ret = chain.invoke("根据各大公司工作过的年份总结工作经验有多少年.格式如下\n工作经验: ?年")
print(ret)


def main():
print("Hello from rag!")
load_doc()


if __name__ == "__main__":
main()

llm.py​

import os
from typing import List, Optional

from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.documents import Document
from langchain_openai import ChatOpenAI
from langchain_qdrant import QdrantVectorStore


def DeepSeek():
return ChatOpenAI(
model="deepseek-v4-flash",
api_key=os.environ.get("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
)


def get_embeddings():
return DashScopeEmbeddings(
dashscope_api_key=os.environ.get("dashscope"),
model="text-embedding-v1",
)


def QdrantVecStoreFromDocs(docs: List[Document], collection_name: str,
qdrant_url: Optional[str] = None,
qdrant_api_key: Optional[str] = None):
if qdrant_url is None:
qdrant_url = "http://localhost:6333"
embeddings = get_embeddings()
vectordb = QdrantVectorStore.from_documents(
docs, embeddings, collection_name=collection_name,
url=qdrant_url, api_key=qdrant_api_key,
)
return vectordb

prompt.py​

RAGPrompt = """
You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, just say that you don't know,
don't try to make up an answer.
Use three sentences maximum and keep the answer concise.
Question: {question}
Context: {context}
Answer:
"""

pyproject.toml​

[project]
name = "rag"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"langchain>=1.3.11",
"langchain-unstructured>=0.1.0",
"langchain-openai>=1.3.3",
"langchain-text-splitters>=0.3.0",
"langchain-community>=0.3.0",
"nltk>=3.9.4",
"python-docx>=1.2.0",
"unstructured>=0.23.1",
"langchain-qdrant>=1.1.0",
"dashscope>=1.0.0",
]

总结​

LangChain 的 LCEL 写 RAG pipeline 非常简洁,配合 Docker 运行的 Qdrant 和 DeepSeek 的 API,从文档到结构化输出只要几十行代码。这套方案不仅适用于简历解析,合同审核、知识库 QA 等场景也能直接复用。