
1. 幻觉代码到底是怎么被喂出来的AI 编程助手写代码这件事现在几乎每个团队都在用。但只要你用得稍微深一点就一定遇到过这种情况它给你一段看起来无比合理的代码import 了一个根本不存在的包调用了一个签名完全对不上的方法或者引用了一个三年前就已经废弃的 API。你复制粘贴运行报错回头找它它非常诚恳地道歉然后给你另一段同样错误的代码。这不是模型笨这是信息源的问题。大语言模型在编程场景下的知识来源本质上就两块训练时背下来的参数化记忆以及推理时通过上下文窗口看到的外部信息。前者是模糊的、有截止日期的、会混淆版本的后者才是精确的、可验证的、实时的。所谓幻觉代码绝大多数情况下就是模型在用第一块知识硬答第二块知识该回答的问题。我举个特别典型的例子。假设你问助手用某个云厂商的对象存储 SDK 上传一个文件并设置生命周期规则。模型在训练数据里见过这个 SDK 的 v2 版本、v3 版本、甚至某个已经下线的预览版本它们的类名、方法链、参数顺序都不一样。模型不会告诉你我不确定你用的是哪个版本它会挑一个它印象最深的版本给你写出来。结果就是你拿着 v3 的文档去跑 v2 的代码或者反过来。这个问题的根源不在于模型能力不够而在于知识供给的方式不对。你把一本 5000 页的、包含 20 个版本文档的说明书塞进它脑子里然后指望它在回答时自动挑出正确的那一页——这本身就是在为难它。所以真正要解决的问题是怎么在模型生成代码的那一刻把当前项目实际使用的那个版本、那个框架、那个云服务的精确文档精准地送到它面前。这就是 MCPModel Context Protocol这类协议以及各大云厂商知识检索架构要解决的核心命题。标题里提到的Google 官方文档 MCP和三大云厂商知识检索架构本质上都是在做同一件事把权威文档变成模型可以实时查询的外部知识源而不是让它靠记忆瞎猜。这篇文章我会从从业者的视角把这件事拆开讲透。包括 MCP 到底解决了什么、文档检索架构应该怎么设计、三大云厂商在这件事上的思路差异、以及我自己在搭建这类系统时踩过的坑和总结出来的实操方法。不管你是刚开始接触 AI 编程助手的开发者还是正在给团队搭建内部知识检索系统的工程师应该都能从里面找到能直接抄作业的东西。2. MCP 协议到底解决了什么问题2.1 从塞上下文到按需查询的范式转变在 MCP 出现之前大家解决文档供给问题的主流做法是塞上下文。也就是在提问的时候把相关文档片段拼接到 prompt 里一起发给模型。RAG检索增强生成本质上就是这个思路的工程化版本先把文档切片、向量化、存进向量数据库用户提问时检索出最相关的几个片段拼进上下文。这个方案能用但有几个绕不过去的坎。第一个坎是检索精度。向量检索是基于语义相似度的它擅长找意思相近的内容但不擅长找精确匹配的内容。你问某个方法的第三个参数是什么向量检索可能给你返回一段讲这个方法整体用法的概述而不是那个参数的精确定义。对于代码场景来说参数类型、方法签名、返回值这些信息差一个字就是编译错误。第二个坎是上下文预算。你不可能把整个文档库都塞进上下文窗口。检索出来的片段数量有限而代码问题往往需要跨多个文档页面才能回答完整。比如怎么配置一个带自定义域名的 HTTPS 负载均衡这个问题可能涉及负载均衡器文档、证书管理文档、DNS 配置文档三个部分向量检索很难一次性把这三块都精准捞出来。第三个坎是时效性。文档是会更新的API 是会变的。向量数据库里的内容需要定期重新索引而这个更新周期往往跟不上文档的实际变化速度。MCP 的思路和 RAG 有本质区别。RAG 是我猜你需要什么提前帮你捞出来MCP 是我告诉你有哪些工具可以用你需要什么自己去查。打个比方。RAG 像是你进了一家餐厅服务员根据你的口味偏好直接给你端上来几个菜。MCP 像是你进了一家自助餐厅服务员告诉你左边是热菜区右边是冷菜区后面是甜品区你想吃什么自己去拿。这个区别在代码场景下特别关键。因为代码问题往往是多轮、递进、需要精确信息的。模型需要能够主动发起查询我要查这个类的构造函数签名、我要查这个方法在 v3 版本里的参数列表、我要查这个错误码的含义。这些查询是模型根据当前对话上下文动态决定的而不是提前检索好的。2.2 MCP 的核心抽象工具、资源与提示MCP 协议本身并不复杂它的核心抽象就三个工具Tools、资源Resources、提示Prompts。工具是模型可以主动调用的函数。比如搜索文档是一个工具获取某个页面的完整内容是一个工具列出某个 API 的所有方法也是一个工具。模型在生成回答的过程中可以决定调用哪个工具、传什么参数然后根据返回结果继续生成。资源是模型可以读取的数据。和工具的区别在于资源是被动的、只读的模型不能通过资源去执行什么操作只能读取内容。比如某个文档页面的原始 Markdown 内容、某个 API 的 OpenAPI 规范文件这些都可以作为资源暴露给模型。提示是预定义的模板用来引导模型在特定场景下的行为。比如代码审查是一个提示模板API 迁移指南是另一个。提示的作用是把领域专家的经验固化下来让模型在面对特定任务时能够按照最佳实践来操作。对于文档检索这个场景来说最核心的是工具。你需要设计一组工具让模型能够搜索文档输入查询词返回相关页面列表获取页面内容输入页面 ID返回完整内容列出某个 API 的所有端点获取某个端点的详细参数说明检查某个 API 在特定版本中的可用性这组工具的设计质量直接决定了整个系统的效果。工具设计得好模型就能像一个有经验的开发者一样一步步查到它需要的信息工具设计得不好模型就会在各种无关结果里打转最后还是靠猜。2.3 为什么 MCP 比直接塞文档更靠谱我实测下来MCP 方案相比直接塞文档在代码场景下有四个明显的优势。第一是精确性。当模型通过工具查询某个方法的参数列表时它拿到的是结构化的、精确的数据而不是一段模糊的自然语言描述。这个区别在生成代码时是决定性的。模型看到的是upload(bucket, key, file, options)这样的精确签名而不是upload 方法接受桶名、键名和文件对象这样的描述。第二是可验证性。模型通过工具查到的每一条信息都有明确的来源。如果生成的代码有问题你可以回溯到是哪个文档页面提供了错误信息或者模型在哪个环节理解错了。这比模型记忆里的某个版本要可追溯得多。第三是时效性。文档更新后只需要更新工具背后的数据源模型下次查询时拿到的就是最新内容。不需要重新训练模型也不需要重新索引向量数据库。第四是上下文效率。模型只在需要的时候才去查询不需要的信息不会占用上下文窗口。这对于处理复杂问题特别重要因为复杂问题往往需要多轮查询如果每轮都塞一大堆文档进去上下文很快就满了。当然MCP 方案也不是没有代价。最大的代价是延迟。每次工具调用都是一次网络请求模型需要等待返回结果才能继续生成。如果一个问题需要查询五六次累积的延迟就会比较明显。这个后面讲优化的时候会详细说。3. 文档检索架构的核心设计要点3.1 文档预处理从 HTML 到结构化知识不管你用不用 MCP文档检索系统的第一步都是把原始文档变成机器可读、可检索的结构化数据。这一步做得好不好直接决定了后面所有环节的上限。大多数官方文档都是以 HTML 形式发布的。直接拿 HTML 去检索效果会很差因为 HTML 里充满了导航栏、侧边栏、页脚、广告、相关链接这些噪音。你需要先把正文内容提取出来。我一般用这样的流程# 文档预处理的核心步骤示意 # 1. 抓取原始 HTML # 2. 提取正文内容去掉导航、侧边栏、页脚 # 3. 转换为 Markdown保留代码块、表格、标题层级 # 4. 按语义单元切片不是按固定字数切 # 5. 为每个切片生成元数据所属页面、API 名称、版本号等 # 6. 存入检索索引这里有几个关键决策点。切片策略是最重要的。很多人习惯按固定字数切片比如每 500 字切一段。这在代码文档场景下是灾难性的因为一个方法的参数说明可能刚好被切到两段里检索的时候只能捞到一半。我的做法是按语义单元切片。一个 API 端点的完整说明是一个切片一个配置项的完整说明是一个切片一个代码示例是一个切片。切片的边界由文档的结构决定而不是由字数决定。具体来说我会用标题层级作为主要边界每个三级标题下的内容作为一个切片如果内容太长再按四级标题细分。元数据设计是第二个关键点。每个切片除了内容本身还需要携带足够的元数据让检索时能够精确过滤。我一般会包含这些字段元数据字段说明用途source_url原始文档地址溯源和引用page_title页面标题展示和排序section_path章节路径如API 参考 对象存储 上传文件上下文理解api_name涉及的 API 或方法名精确匹配version适用的版本号版本过滤content_type内容类型概念说明/API 参考/代码示例/教程结果分类last_updated最后更新时间时效性排序有了这些元数据检索时就可以做很精细的过滤。比如模型问v3 版本的上传方法检索时就可以加上versionv3和api_nameupload的过滤条件而不是在所有版本的所有方法里做语义搜索。代码块的处理需要特别小心。代码块在 Markdown 里是用 包裹的切片的时候要确保代码块不被切断。我见过太多系统把一段完整的代码示例切成两半模型拿到半截代码生成出来的东西自然也是半截的。3.2 检索策略混合检索才是正解纯向量检索在代码文档场景下是不够用的。原因前面说过代码问题往往需要精确匹配而向量检索擅长的是语义相似。我的做法是混合检索向量检索 关键词检索 结构化过滤三路并行然后融合排序。向量检索负责语义相关。比如用户问怎么把文件传到云上向量检索能找到对象存储上传相关的文档即使文档里没有出现传到云上这几个字。关键词检索负责精确匹配。比如用户问upload 方法的 options 参数关键词检索能精确找到包含 upload 和 options 的文档片段。这里我一般用 BM25 或者类似的算法对代码标识符类名、方法名、参数名做特殊加权。结构化过滤负责条件筛选。比如用户指定了版本号、指定了某个云服务、指定了某种编程语言这些条件直接作为过滤条件应用到检索结果上。三路结果融合的时候我用的是加权倒数排名融合Weighted Reciprocal Rank Fusion。简单说就是每个结果在三路检索里各有一个排名根据排名的倒数加权求和得到最终分数。权重可以根据场景调整代码场景下我一般把关键词检索的权重调高一些。# 混合检索的融合排序示意 def hybrid_search(query, filters, top_k10): # 三路检索 vector_results vector_search(query, filters, top_k50) keyword_results keyword_search(query, filters, top_k50) structured_results structured_filter(filters, top_k50) # 加权倒数排名融合 scores {} for rank, doc in enumerate(vector_results): scores[doc.id] scores.get(doc.id, 0) 0.3 / (rank 60) for rank, doc in enumerate(keyword_results): scores[doc.id] scores.get(doc.id, 0) 0.5 / (rank 60) for rank, doc in enumerate(structured_results): scores[doc.id] scores.get(doc.id, 0) 0.2 / (rank 60) # 按分数排序返回 return sorted(scores.items(), keylambda x: -x[1])[:top_k]这里的 60 是平滑常数防止排名靠前的结果分数过高。0.3、0.5、0.2 是权重可以根据实际效果调整。我实测下来关键词检索权重高一些对代码场景更友好。3.3 工具设计让模型像开发者一样查文档MCP 工具的设计核心原则是让模型能够像人类开发者一样一步步缩小范围最终定位到精确信息。人类开发者查文档的流程通常是这样的先搜索一个大概的关键词看看有哪些相关页面点进最相关的页面浏览一下结构找到具体的章节仔细阅读如果涉及代码复制示例代码根据实际情况修改。MCP 工具应该支持这个流程的每一步。我一般会设计这几个工具search_docs输入查询词和可选的过滤条件返回相关页面列表包含标题、摘要、URL。这是入口工具模型用它来发现有哪些相关文档。get_page输入页面 ID返回该页面的完整内容Markdown 格式。模型用它来阅读具体页面的详细内容。list_apis输入服务名和可选的版本号返回该服务下所有 API 的列表包含名称、简要说明。模型用它来了解某个服务提供了哪些能力。get_api_detail输入 API 名称和版本号返回该 API 的完整说明包含参数列表、返回值、示例代码、错误码。这是最核心的工具模型用它来获取生成代码所需的精确信息。check_version输入 API 名称和版本号返回该 API 在该版本中是否可用、是否有变更。模型用它来避免使用已废弃的 API。这五个工具的组合基本覆盖了代码生成场景下的所有文档查询需求。工具的数量不宜过多太多会让模型在选择时犹豫也不宜过少太少会导致模型无法获取足够的信息。提示工具的描述description非常重要。模型是根据工具描述来决定调用哪个工具的。描述要写得清晰、具体说明这个工具适合什么场景、输入参数是什么格式、返回什么内容。我见过很多 MCP 实现效果不好问题就出在工具描述写得太模糊。3.4 版本管理代码场景下的生命线代码场景和普通问答场景最大的区别就是版本。同一个 API 在不同版本里可能完全不同而模型如果搞混了版本生成的代码就是废的。版本管理要在三个层面做。第一个层面是文档层面。每个文档切片都要标注它适用的版本。如果某个 API 在 v2 和 v3 里都有那就存两份切片分别标注 v2 和 v3。检索时根据用户指定的版本过滤。第二个层面是工具层面。工具的参数里要包含版本号模型在调用工具时可以指定版本。如果模型没有指定工具应该返回一个默认版本通常是当前稳定版的结果并在返回内容里明确标注版本号让模型知道它拿到的是哪个版本的信息。第三个层面是提示层面。在系统提示里要明确告诉模型在生成代码之前先确认用户使用的版本。如果不确定先询问用户或者使用 check_version 工具查询。这个提示能显著减少版本混淆的问题。我踩过的一个坑是早期版本的系统里我没有在工具返回内容里标注版本号结果模型有时候会把 v2 的信息当成 v3 的用。后来我在每个工具返回结果的开头都加了一行[Version: v3]这个问题就基本消失了。模型对显式的版本标注非常敏感。4. 三大云厂商知识检索架构的思路差异4.1 思路一文档即服务工具化封装第一种思路我称之为文档即服务。核心思想是把官方文档包装成一组标准化的工具通过 MCP 协议暴露给模型。模型不直接接触文档内容而是通过工具调用来获取信息。这种思路的代表是 Google 官方文档的 MCP 实现。它的特点是工具粒度细。不是简单地提供一个搜索文档的工具而是针对不同类型的文档提供不同的工具。比如 API 参考文档有专门的工具教程文档有专门的工具代码示例有专门的工具。这样模型可以根据问题类型选择最合适的工具。返回结构化数据。工具返回的不是一段文本而是结构化的 JSON 数据。比如查询一个 API 的详情返回的是包含参数列表、返回值类型、示例代码、错误码的结构化对象。模型可以直接从这个结构里提取需要的信息而不需要从自然语言描述里猜。强调版本和时效。每个工具调用都可以指定版本返回结果里也明确标注版本和最后更新时间。这让模型能够做出基于版本的决策。这种思路的优点是精确、可控、可追溯。缺点是工具的设计和维护成本比较高需要针对每种文档类型单独设计工具。4.2 思路二统一检索层语义优先第二种思路我称之为统一检索层。核心思想是建立一个统一的文档检索服务把所有文档都索引进去通过一个统一的检索接口对外提供服务。模型通过这个接口来查询文档接口内部负责处理各种检索策略。这种思路在一些云厂商的实现里比较常见。它的特点是统一的检索入口。不管是什么类型的文档都通过同一个检索接口查询。接口内部会根据查询内容自动选择检索策略向量检索、关键词检索、混合检索。语义理解优先。强调对查询意图的理解。比如用户问怎么创建一个虚拟机系统会理解到这是创建计算实例的意图然后去检索相关的文档而不是简单地匹配虚拟机这个词。结果聚合。检索结果不是单个文档片段而是聚合后的答案。系统会把多个相关片段整合成一个连贯的回答附上引用来源。这种思路的优点是使用简单、覆盖面广。缺点是精确性不如第一种思路特别是在需要精确参数信息的代码场景下聚合后的答案可能会丢失细节。4.3 思路三知识图谱关系驱动第三种思路我称之为知识图谱。核心思想是把文档里的知识抽取成实体和关系构建一个知识图谱然后基于图谱来做检索和推理。这种思路在一些厂商的内部实现里能看到影子。它的特点是实体抽取。从文档里抽取出 API、参数、类、方法、配置项等实体以及它们之间的关系如方法 A 属于类 B、参数 C 是方法 D 的必填参数。关系推理。基于图谱做推理。比如用户问怎么上传文件系统可以从图谱里找到上传文件这个操作涉及的所有实体和关系然后生成一个完整的操作路径。动态更新。文档更新时只需要更新图谱里对应的实体和关系不需要重新索引整个文档库。这种思路的优点是推理能力强、能够回答复杂问题。缺点是构建和维护知识图谱的成本很高而且抽取的准确性直接影响效果。4.4 三种思路的对比与选型建议维度文档即服务统一检索层知识图谱精确性高中高覆盖面中高中实现成本中低高维护成本中低高适合场景API 参考、代码生成通用问答、教程检索复杂推理、跨文档问题版本管理强中强我的选型建议是如果你的核心场景是代码生成优先考虑文档即服务思路。因为代码生成对精确性的要求最高而文档即服务通过工具化封装能够提供最精确的信息。如果预算和人力允许可以在文档即服务的基础上叠加一层统一检索层作为补充用于处理教程类、概念类的查询。知识图谱思路适合大型团队有专门的团队来维护图谱。对于大多数团队来说投入产出比不高。5. 实操从零搭建一个文档检索 MCP 服务5.1 环境准备与依赖安装我以 Python 技术栈为例讲一下怎么从零搭建一个文档检索 MCP 服务。这个服务的目标是把某个云服务的官方文档索引起来通过 MCP 协议暴露给 AI 编程助手让助手在生成代码时能够查询精确的文档信息。先装依赖pip install mcp httpx beautifulsoup4 markdownify chromadb sentence-transformers rank-bm25这里解释一下每个依赖的作用mcpMCP 协议的 Python SDK用来实现 MCP 服务端。httpx异步 HTTP 客户端用来抓取文档页面。beautifulsoup4HTML 解析库用来提取正文内容。markdownify把 HTML 转换成 Markdown保留代码块和表格结构。chromadb向量数据库用来存储和检索文档向量。sentence-transformers生成文本向量。rank-bm25BM25 关键词检索。注意向量模型的选择很关键。代码文档场景下我推荐用专门针对代码训练的向量模型而不是通用的文本向量模型。通用模型对代码标识符的语义理解往往不够好。5.2 文档抓取与预处理第一步是把文档抓下来提取正文转换成 Markdown。import httpx from bs4 import BeautifulSoup from markdownify import markdownify async def fetch_and_clean(url: str) - str: 抓取文档页面并提取正文 Markdown async with httpx.AsyncClient() as client: resp await client.get(url, timeout30) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) # 移除噪音元素 for tag in soup.select(nav, footer, aside, .sidebar, .ad, .related-links): tag.decompose() # 定位正文容器不同文档站点的选择器不同需要根据实际情况调整 main soup.select_one(main, article, .content, #content) if not main: main soup.body # 转换成 Markdown md markdownify(str(main), heading_styleATX) return md这里的关键是正文容器的选择。不同文档站点的 HTML 结构不一样需要针对性地调整选择器。我的经验是先手动打开几个页面用浏览器的开发者工具看一下正文容器的 class 或 id然后写对应的选择器。如果文档站点有 sitemap 或者导航结构可以先把所有页面 URL 抓下来然后批量处理。注意控制抓取频率不要给文档站点造成压力。5.3 语义切片与元数据标注拿到 Markdown 之后下一步是切片。我前面说过要按语义单元切不要按固定字数切。import re from dataclasses import dataclass dataclass class Chunk: content: str source_url: str page_title: str section_path: str api_name: str version: str content_type: str def split_by_sections(md: str, url: str, title: str, version: str) - list[Chunk]: 按标题层级切片 chunks [] # 按三级标题切分 sections re.split(r\n(?### ), md) for section in sections: if not section.strip(): continue # 提取章节标题 lines section.strip().split(\n) heading lines[0].lstrip(# ).strip() if lines[0].startswith(#) else title # 提取 API 名称从标题或内容里找 api_match re.search(r(\w)|(\w)\s*方法|(\w)\s*API, section) api_name api_match.group(1) or api_match.group(2) or api_match.group(3) if api_match else # 判断内容类型 if in section: content_type code_example elif 参数 in section or 返回值 in section: content_type api_reference else: content_type concept chunks.append(Chunk( contentsection.strip(), source_urlurl, page_titletitle, section_pathf{title} {heading}, api_nameapi_name or , versionversion, content_typecontent_type, )) return chunks这段代码是示意性的实际使用时要根据文档的具体结构来调整。核心思路是用标题作为主要切分点用内容特征来标注元数据。5.4 混合检索索引的构建切片完成后需要建立检索索引。我一般建两个索引向量索引和关键词索引。import chromadb from rank_bm25 import BM25Okapi from sentence_transformers import SentenceTransformer # 初始化 chroma_client chromadb.PersistentClient(path./doc_index) collection chroma_client.get_or_create_collection(docs) embed_model SentenceTransformer(your-code-embedding-model) def build_index(chunks: list[Chunk]): 构建向量索引和关键词索引 # 向量索引 texts [c.content for c in chunks] embeddings embed_model.encode(texts, show_progress_barTrue) collection.add( ids[fchunk_{i} for i in range(len(chunks))], embeddingsembeddings.tolist(), documentstexts, metadatas[{ source_url: c.source_url, page_title: c.page_title, section_path: c.section_path, api_name: c.api_name, version: c.version, content_type: c.content_type, } for c in chunks], ) # 关键词索引 tokenized [tokenize(c.content) for c in chunks] bm25 BM25Okapi(tokenized) return bm25 def tokenize(text: str) - list[str]: 针对代码文档的分词 # 保留代码标识符的完整性 tokens re.findall(r[a-zA-Z_]\w*|[0-9]|[^\s\w], text) return [t.lower() for t in tokens]分词这里要特别注意。通用的中文分词器会把upload_file切成upload和file这在代码场景下可能丢失信息。我的做法是用正则表达式提取标识符保留upload_file作为一个完整的 token同时也把它拆开作为两个 token 加入索引。这样既能精确匹配也能部分匹配。5.5 MCP 工具的实现与注册索引建好之后就可以实现 MCP 工具了。from mcp.server import Server from mcp.types import Tool, TextContent server Server(doc-search) server.list_tools() async def list_tools(): return [ Tool( namesearch_docs, description搜索文档。输入查询词返回相关文档片段列表。适合在不确定具体 API 名称时使用。, inputSchema{ type: object, properties: { query: {type: string, description: 查询词}, version: {type: string, description: 文档版本如 v3}, content_type: {type: string, description: 内容类型api_reference/code_example/concept}, }, required: [query], }, ), Tool( nameget_api_detail, description获取指定 API 的详细说明包括参数列表、返回值、示例代码。适合在已知 API 名称时使用。, inputSchema{ type: object, properties: { api_name: {type: string, description: API 或方法名称}, version: {type: string, description: 版本号}, }, required: [api_name], }, ), ] server.call_tool() async def call_tool(name: str, arguments: dict): if name search_docs: results hybrid_search( queryarguments[query], versionarguments.get(version), content_typearguments.get(content_type), ) return [TextContent(typetext, textformat_results(results))] elif name get_api_detail: results get_api_by_name( api_namearguments[api_name], versionarguments.get(version), ) return [TextContent(typetext, textformat_api_detail(results))]工具描述我写得比较详细明确说明了每个工具适合什么场景。这是有意为之的因为模型是根据描述来选择工具的。描述写得越清楚模型选错工具的概率就越低。5.6 与 AI 编程助手的集成测试MCP 服务写好后需要配置到 AI 编程助手里。不同的助手配置方式不一样但基本都是在配置文件里加上 MCP 服务的启动命令。配置好之后我一般会做几组测试来验证效果测试一精确 API 查询。问助手用某个 SDK 上传文件的方法签名是什么看它是否能通过工具查到精确的参数列表。测试二版本区分。问助手v2 和 v3 的上传方法有什么区别看它是否能分别查询两个版本并对比。测试三代码生成。让助手生成一段使用某个 API 的代码然后检查生成的代码是否与文档一致。测试四错误处理。问助手这个 API 返回某个错误码时应该怎么处理看它是否能查到错误码的说明。这几组测试基本能覆盖核心场景。如果测试中发现模型没有调用工具或者调用了错误的工具就需要回头调整工具描述或者系统提示。6. 常见问题与排查技巧实录6.1 模型不调用工具直接凭记忆回答这是最常见的问题。模型明明有工具可用但它就是不用直接凭训练时的记忆给你生成代码。原因通常有三个一是工具描述写得不够清楚模型不知道什么时候该用二是系统提示里没有强调要使用工具三是模型本身的能力限制某些模型对工具调用的支持不够好。解决办法首先在系统提示里明确写在回答任何与 API 使用相关的问题之前必须先通过工具查询文档。其次把工具描述写得更加具体最好包含使用示例。最后如果模型本身对工具调用支持不好考虑换一个对工具调用支持更好的模型。我实测下来系统提示里加一句不要凭记忆回答 API 相关问题必须先查询文档能显著提升工具调用率。6.2 检索结果不相关模型查了等于没查模型调用了工具但返回的结果和问题不相关。这通常是检索环节的问题。排查思路先看查询词是什么。如果模型传的查询词太宽泛比如只传了上传两个字检索结果自然不精确。这时候需要优化工具描述引导模型传更具体的查询词。如果查询词没问题那就是检索算法的问题。检查向量模型是否适合代码场景检查关键词检索的分词是否正确检查元数据过滤是否生效。我遇到过一个典型问题文档里 API 名称是upload_object但用户习惯说上传文件。向量检索能找到相关文档但关键词检索找不到因为上传文件和upload_object没有字面重叠。解决办法是在文档预处理阶段为每个 API 生成同义词标注把上传文件、上传对象这些常见说法也加入关键词索引。6.3 版本混淆生成的代码用错版本模型查到了文档但用错了版本。比如用户用的是 v2模型却按 v3 的文档生成了代码。这个问题要从三个层面解决。第一在系统提示里要求模型先确认版本。第二在工具返回结果里显式标注版本号。第三如果用户没有指定版本工具返回默认版本的结果时要在结果开头明确写以下内容基于 v3 版本如果你使用的是其他版本请指定版本号重新查询。我还会在工具里加一个check_version功能让模型可以主动查询某个 API 在哪些版本里可用。这样模型在不确定的时候可以先去查一下版本信息。6.4 工具调用延迟高影响体验每次工具调用都是一次网络请求如果检索服务响应慢整体体验就会很差。优化方向有几个。第一把向量检索和关键词检索做成并行而不是串行。第二对常见查询做缓存比如某个 API 的详情查询如果短时间内多次查询同一个 API直接返回缓存结果。第三优化向量索引使用更快的近似最近邻算法。第四控制返回结果的数量不要一次返回几十个片段只返回最相关的三到五个。我实测下来把返回结果从 10 个降到 5 个延迟能降低 30% 左右而效果几乎没有下降。因为模型真正用到的往往就是最相关的那两三个片段。6.5 常见问题速查表问题现象可能原因排查方法解决方案模型不调用工具工具描述不清/系统提示未强调检查工具描述和系统提示优化描述加显式提示检索结果不相关查询词太宽泛/检索算法不适配查看模型传入的查询词优化工具描述调整检索策略版本混淆未标注版本/未确认版本检查工具返回内容显式标注版本加版本确认提示延迟高串行检索/结果过多测量各环节耗时并行检索加缓存减少返回数量代码块被切断切片策略不当检查切片边界按语义单元切片保护代码块工具调用失败参数格式错误/服务异常查看工具调用日志加参数校验加错误处理6.6 几个独家避坑技巧技巧一在工具返回结果里加使用提示。比如返回 API 详情时在末尾加一句注意此方法在 v3 中已废弃推荐使用 xxx 方法。这个提示能帮助模型做出更好的决策。技巧二为常见问题预置快捷查询。比如如何上传文件、如何创建实例这些高频问题预先配置好查询路径模型调用时直接返回预置结果不走完整检索流程。这能显著降低延迟。技巧三定期分析工具调用日志。看看模型最常调用哪些工具、传什么查询词、哪些查询返回了空结果。这些数据是优化系统的金矿。我通过分析日志发现模型经常把 API 名称拼错于是在检索时加了模糊匹配效果提升明显。技巧四不要追求一次查询就返回完美结果。让模型多查几次每次查一个具体的点比一次返回一大堆信息效果更好。因为模型在多轮查询中能够根据上一轮的结果调整下一轮的查询逐步逼近精确答案。7. 我在这件事上的几点真实体会搭这套东西的过程中我最大的体会是文档检索系统的效果八成取决于文档预处理的质量两成取决于检索算法。很多人把精力花在调检索算法上却忽略了文档切片和元数据标注这些基础工作。实际上如果切片切得好、元数据标得准哪怕用最简单的检索算法效果也不会差。另一个体会是不要试图让模型一次就拿到所有信息。我早期设计的工具总想一次返回尽可能多的内容结果模型被大量无关信息干扰反而抓不住重点。后来我把工具设计成小步快跑的模式每次只返回最相关的少量信息让模型自己决定下一步查什么效果反而更好。还有一个很实际的体会版本管理是代码场景的生命线。我在版本管理上踩过的坑比在其他所有环节加起来都多。如果你的文档检索系统要服务代码生成场景一定要在版本管理上投入足够的精力。每个切片标版本、每个工具支持版本参数、每次返回标注版本号这三件事一个都不能少。最后说一个我觉得很有用的实践把工具调用日志和最终生成的代码关联起来分析。当生成的代码有问题时回溯去看模型调用了哪些工具、拿到了什么信息、在哪一步理解错了。这个分析过程能帮你发现系统的薄弱环节比盲目优化有效得多。我通过这种方式发现了好几个隐藏的问题比如某个 API 的文档页面结构特殊切片时把参数说明切到了两个片段里导致模型只拿到了一半参数信息。这套东西还在持续迭代中文档在更新模型在升级检索策略也在调整。但核心思路是不变的把权威文档变成模型可以实时查询的外部知识源让它在生成代码之前先查证而不是凭记忆瞎猜。这个思路一旦落地幻觉代码的问题就能得到根本性的改善。