跳到主要内容

85 篇博文 含有标签「AI」

Artificial Intelligence 相关文章

查看所有标签

Agent 安全防护:ToolGuardian 深度解析

· 阅读需 12 分钟

引言​

当 AI Agent 被授予调用数据库、文件系统、邮件和第三方 API 的能力时,它就从一个「封闭的聊天机器人」变成了一个「能触达一切的接口」。这把双刃剑的锋利程度,在 2025-2026 年多次安全事件中已被验证:

  • Salesforce Einstein 被注入攻击,delete_contact 接口批量删除客户记录
  • GitHub Copilot Chat 在处理 Issue 时不慎将内部 API Key 拼入工具调用参数
  • AutoGPT 第三方插件通过工具调用链获取主机系统权限

安全不再是附加功能,而是 Agent 走向生产环境的准入门槛。 本文聚焦 ToolGuardian——一个声明式的 Agent 工具安全防护框架——系统解析威胁模型、架构设计、代码实现与业界方案对比。

Agent 推理与执行分离:构建可维护的 AI 智能体架构

· 阅读需 11 分钟

面向资深后端工程师,深入 Agent 架构中的推理与执行分离模式。

从单体 Agent 的困境说起​

假设你构建了一个简单的 ReAct Agent:LLM 在同一个循环中同时完成「思考做什么」和「具体怎么做」。代码往往长这样:

while True:
thought = llm.generate(f"当前状态:{state},下一步做什么?")
action = parse_action(thought) # 执行工具调用
result = execute_tool(action)
state += f"\n执行结果:{result}"

看起来简洁,但部署到生产后会遇到一系列问题:

  • 调试困难:一个失败的步骤到底是推理错了还是执行错了?日志里 Thought/Action/Observation 混在一起,难以定位根因。
  • 测试困难:你想 mock 工具调用来测试推理逻辑,但推理和执行在一个函数里,只能端到端测试。
  • 上下文污染:上一次执行的冗余输出挤占了下一次推理的窗口。
  • 无法重放:生产环境出了 bug,你想重放当时的推理链路——但执行结果不可重复(API 超时、数据库状态变了)。

这些问题的根源在于:推理(Reasoning)和执行(Execution)是两种不同的关注点,却被耦合在了同一个循环中。

vLLM 架构详解:PagedAttention、Continuous Batching 与生产级推理优化

· 阅读需 12 分钟

本文是 AI Infra 系列的实战篇,前篇《AI Infra 到底是什么》梳理了 AI 基础设施的四层架构,本文聚焦推理层最核心的开源项目——vLLM。

前言:为什么 vLLM 是推理引擎的标杆​

大模型推理引擎有很多:TGI(HuggingFace)、TensorRT-LLM(NVIDIA)、SGLang、CTranslate2……但在开源社区中,vLLM 是目前最流行、生态最好的通用推理引擎。

它解决了 LLM 推理中三个核心痛点:

  1. 显存浪费:传统方案 KV Cache 需要预分配最大序列长度,实际序列通常短得多,造成高达 60-80% 的显存浪费
  2. 吞吐量低:静态批处理无法动态管理请求,GPU 利用率难以提高
  3. 调度僵化:不支持抢占、不可用请求灵活排队

vLLM 凭借 PagedAttention 和 Continuous Batching 两项核心技术,在上述三个维度都实现了数量级的提升。

MCP 协议深度解析:架构设计、传输层与工具编排最佳实践

· 阅读需 12 分钟

本文是 Agent 协议系列的第二篇,前篇《Function Calling 诞生的背景》探讨了函数调用能力的起源,本篇聚焦更通用的协议层——MCP。

为什么需要 MCP?​

Function Calling 解决了「LLM 如何调用外部函数」的问题,但它有一个根本性局限:每个模型厂商的函数定义格式不同。OpenAI 有 tools 参数,Anthropic 用 tools 但 schema 略有差异,Google Gemini 用 tools / tool_config,Llama 系列模型需要特定的 prompt 模板。

这意味着你的工具代码需要为每个模型写一个适配层。更麻烦的是,工具本身的注册、发现、安全策略并没有一个统一的标准——每个 Agent 框架都在自己造轮子。

MCP(Model Context Protocol) 就是来解决这个问题的。它由 Anthropic 提出,本质上是一个通用的、模型无关的上下文与工具交互协议,定义了 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 的地基。本文逐项拆解它们的原理、实现路径和工程上的坑。

Windows VSCode 调用 WSL 中的 Hermes Agent 实战

· 阅读需 9 分钟

背景​

这套组合的出现有一个很现实的理由:

  • VSCode 是 Windows 上最好的编辑器之一,生态丰富,Remote 插件体系成熟
  • WSL 提供了完整的 Linux 开发环境,项目依赖、脚本、网络工具链都在 WSL 里
  • Hermes Agent 跑在 Linux 上最顺畅,它需要访问项目的 Git 历史、文件结构、终端环境

所以很自然的,你会想让 VSCode 连进 WSL,然后在编辑器里直接和 Hermes 交互——写完代码让它审,遇到问题让它查,重构代码让它帮。

这篇文章就讲清楚这条路怎么打通,以及打通之后能怎么用。