跳到主要内容

AI-SDK 工具与 Agent · 跨端共享的提示词设计

30 秒导读: 这一章讲 packages/tools-ai-sdk —— 它把 Context7 的两个动作(把库名解析成库 ID、再按库 ID 拉文档)包装成 Vercel AI SDKtool(),再把它们塞进一个会自动多步循环的 Context7Agent。但真正的主角不是这几十行胶水代码,而是提示词:同一套「先 resolve 再 query、每问最多 3 次、库 ID 必须是 /org/project、一次只查一个概念」的规则,被有意地抄写到三处不同载体(这里的 prompts、02 章的 MCP 工具 description、05 章的 skill/rule)——它其实是一份跨端复用的「工作流契约」


1. 这是什么(零基础也能懂)

一句话定义: @upstash/context7-tools-ai-sdk 是一个小包,让你用一行代码就能把 Context7 的查文档能力,接进任何基于 Vercel AI SDK 写的 Agent。

解决什么问题 / 给谁用: 假设你在用 AI SDK 写一个自己的编码助手(generateText({ model, tools }) 那套)。你想让它能查到最新的库文档,又不想自己去调 Context7 的 HTTP 接口。这个包给你两样现成的东西:

  • 两个开箱即用的工具对象(resolveLibraryId() / queryDocs()),直接丢进你的 tools: {}
  • 一个开箱即用的 Agent(Context7Agent),连多步循环和系统提示都替你配好了。

用起来什么样: 最小用法就是把两个工具塞进 generateText,SDK 会自己决定何时调用它们。

// 摘自 src/tools/resolve-library-id.ts:16-31 的 JSDoc 示例(真实可跑)
import { resolveLibraryId, queryDocs } from '@upstash/context7-tools-ai-sdk';
import { generateText, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';

const { text } = await generateText({
model: openai('gpt-4o'),
prompt: 'Find React documentation about hooks',
tools: {
resolveLibraryId: resolveLibraryId(), // 库名 → 库 ID
queryDocs: queryDocs(), // 库 ID → 文档
},
stopWhen: stepCountIs(5), // 最多循环 5 步
});

一句话直觉: 模型自己不知道 /facebook/react 这个内部 ID 长什么样,所以永远得先问路(resolve)再取货(query)。这个包做的,就是把「问路」「取货」两个动作包成模型能调的按钮,再用一段提示词反复叮嘱它「按顺序按、别乱按、别按太多次」。

本章不重述这两个工具底层怎么发 HTTP 请求、认证怎么走——那是最底层的 SDK 与 API 契约,见 01 章。这里只讲「怎么把 SDK 调用接进 AI SDK」和「提示词怎么设计」。


2. 顶层全景(它大概怎么转)

这个包一共就三层,加一份贯穿全场的提示词。 先看文件职责:

部件干什么在哪个文件
resolveLibraryId() / queryDocs()用 AI SDK 的 tool() 把 SDK 调用包成工具src/tools/resolve-library-id.tssrc/tools/query-docs.ts
Context7ToolsConfig工具/Agent 的配置类型(就一个可选 apiKey)src/tools/types.ts:4
Context7Agent继承 ToolLoopAgent,把两工具 + 提示词 + 循环上限打包src/agents/context7.ts:38
提示词一组SYSTEM_PROMPT / AGENT_PROMPT / 两个工具 *_DESCRIPTIONsrc/prompts/system.ts
统一出口把上面全部 re-export,外加 SDK 的类型src/index.ts

主线走一遍(高层,不进代码): 一次「查 React hooks 文档」的调用是这样流动的——

用户 prompt: "Find React hooks docs"


┌─────────────────────────────┐
│ Context7Agent (ToolLoopAgent)│ ← 注入 AGENT_PROMPT 当 instructions
│ 循环上限 = stepCountIs(5) │
└─────────────────────────────┘
│ 模型读提示词,决定第 1 步

① resolveLibraryId("React") ──► 返回候选库列表(含 /reactjs/react.dev 等)
│ 模型挑出最佳库 ID

② queryDocs("/reactjs/react.dev", "hooks") ──► 返回文档片段


③ 模型综合文档,写出带代码示例的回答

怎么读这张图: 从上到下是时间顺序。①②是模型在循环里自己发起的两次工具调用,③是循环结束后的最终作答。「先①后②」不是代码硬编码的,而是提示词逼出来的——这正是下一节的重点。


3. 核心机制(逐个,由浅入深)

3.1 用 tool() 把一个 SDK 调用包成 AI SDK 工具

它要解决的小问题: AI SDK 认的「工具」是一个特定形状的对象——要有 description(告诉模型这工具干嘛)、inputSchema(参数的 Zod schema)、execute(真正执行的函数)。而 Context7 SDK 给的是一个普通的 client.searchLibrary(...) 异步调用。中间需要一层适配。

思路: 写一个工厂函数 resolveLibraryId(config),内部 new Context7({ apiKey }),然后用 SDK 的 tool() 把三件套拼出来。参数校验交给 Zod,执行体里 try/catch 兜底,失败也返回字符串(而不是抛异常),这样模型能读到错误信息、自行重试。

真实实现:

// src/tools/resolve-library-id.ts:33-66 resolveLibraryId
export function resolveLibraryId(config: Context7ToolsConfig = {}) {
const { apiKey } = config;
const getClient = () => new Context7({ apiKey }); // 无 key 时回落到环境变量

return tool({
description: RESOLVE_LIBRARY_ID_DESCRIPTION, // ← 提示词从 @prompts 注入
inputSchema: z.object({ query: z.string()..., libraryName: z.string()... }),
execute: async ({ query, libraryName }) => {
try {
const results = await client.searchLibrary(query, libraryName, { type: "txt" });
if (!results || results.length === 0) return `No libraries found ...`; // 字符串,非异常
return results;
} catch (error) { return `Error searching for libraries: ...`; }
},
});
}

queryDocs 是同一个模子(src/tools/query-docs.ts:33-68),只是把 searchLibrary 换成 client.getContext(query, libraryId, ...)

几个值得记的细节:

  • inputSchema 里的 .describe() 也是提示词。 参数说明不是给人看的注释,而是喂给模型的指令。例如 libraryId 的说明直接写死了格式契约:

    Exact Context7-compatible library ID (e.g., '/mongodb/docs', ...) ... in the format '/org/project' or '/org/project/version'src/tools/query-docs.ts:41-44query 参数的说明更是把「单概念一次查询」的规则写进了 schema:src/tools/query-docs.ts:45-49

  • 配置只有一个字段。 Context7ToolsConfig 全部内容就是可选的 apiKey(src/tools/types.ts:4-9);不传就用 CONTEXT7_API_KEY 环境变量。
  • 错误不抛,返回字符串。 让模型「看见」错误、在循环里自愈,而不是让整个 generateText 崩掉。

3.2 Context7Agent:把循环、工具、提示词一次打包

它要解决的小问题: 3.1 的两个工具还得用户自己塞进 generateText、自己设循环上限、自己写系统提示。对只想「拿来即用」的人太啰嗦。

思路: 继承 AI SDK 的 ToolLoopAgent(自带「调工具 → 读结果 → 再决定」的多步循环),在构造函数里把默认值全填好。

真实实现:

// src/agents/context7.ts:38-63 Context7Agent
export class Context7Agent extends ToolLoopAgent<never, ToolSet> {
constructor(config: Context7AgentConfig) {
const {
model,
stopWhen = stepCountIs(5), // ① 默认最多循环 5 步
instructions,
apiKey,
tools,
...agentSettings
} = config;
super({
...agentSettings,
model,
instructions: instructions || AGENT_PROMPT, // ② 默认注入 AGENT_PROMPT
tools: {
...tools, // ③ 允许用户加自己的工具
resolveLibraryId: resolveLibraryId({ apiKey }),
queryDocs: queryDocs({ apiKey }),
},
stopWhen,
});
}
}

三个关键设计,对号入座:

记号机制作用
stopWhen = stepCountIs(5)用「步数」当循环刹车,防止模型无限调工具;用户可覆盖
instructions || AGENT_PROMPT不传就注入内置的强约束工作流提示;传了就完全尊重用户的
{ ...tools, resolveLibraryId, queryDocs }用户自带的工具和 Context7 两工具并存,而非二选一

stepCountIs(5) 和「≤3 次」是两回事,别混。 stepCountIs代码层的硬刹车(SDK 数循环步数);而「每个工具每问不超过 3 次」是提示词层的软约束(靠模型自觉,见 3.3)。一硬一软,双保险。

apiKey 的来源链: Context7AgentapiKey → 传给 resolveLibraryId({ apiKey }) / queryDocs({ apiKey }) → 传给 new Context7({ apiKey }) → 若为 undefined,SDK 内部回落到 CONTEXT7_API_KEY 环境变量(HTTP/认证细节见 01 章)。

3.3 横切主线:提示词工程 = 把「工作流」写成自然语言约束

这是本章真正的核心。 前面的工具/Agent 只是骨架;让它可靠工作的,是 src/prompts/system.ts 里那几段字符串。它们把一套流程规则翻译成模型能读懂的强指令。

为什么需要它: 模型很聪明,但没约束时会「抄近路」——比如跳过 resolve 直接猜库 ID、或反复对同一问题狂调工具。提示词的活,就是把这些坏习惯一条条堵死。

AGENT_PROMPT 怎么下命令(src/prompts/system.ts:20-47):

  • 开头就是全大写的 CRITICAL WORKFLOW - YOU MUST FOLLOW THESE STEPS,把四步(resolve → 选最佳库 → query → 带例作答)编号列死。
  • 第 2 步给出选库的打分维度:官方源、名字相似度、描述相关性、源可信度(High/Medium 更好)、代码片段覆盖、benchmark 分。
  • 结尾 IMPORTANT 三连:必须先 resolveLibraryId、不准跳过它、每个工具每问不超过 3 次、永远注明用了哪个库 ID。

同一套规则,还散落在工具 description 里当「就近提醒」。 除了顶层的 AGENT_PROMPT,每个工具自己的 description 也复述关键约束——因为模型在决定要不要调这个工具的那一刻,读到的是工具 description,而不是遥远的系统提示。

规则AGENT_PROMPT在工具 *_DESCRIPTION
先 resolve 再 query第 1 步 + IMPORTANT 首条You MUST call this function before 'queryDocs' ...(system.ts:54)
库 ID = /org/project隐含在示例里description 明写 /org/project' or '/org/project/version'(system.ts:5480)
每问 ≤ 3 次Do not call either tool more than 3 times(system.ts:46)两个 description 各自复述(system.ts:7382)
用户已给库 ID 则可跳过 resolve——UNLESS the user explicitly provides a library ID ...(system.ts:54)

还有一个更轻量的 SYSTEM_PROMPT(src/prompts/system.ts:8-15): 三句话版本,给「不需要强流程、只想要个文档助手」的场景。AGENT_PROMPT 是它的「加强约束版」。这两者的分工,本身就体现了「提示词按场景分级」的思路。


4. 「工作流契约」:同一套规则,跨三种载体复用

把视野拉高:Context7 把「怎么正确用这两个工具」当成一份契约,在整个仓库里反复抄写到不同载体。 这不是重复劳动,而是刻意为之——因为同一个 Agent 规则要在不同运行时(AI SDK / MCP 客户端 / 各家 skill 系统)都生效,而这些运行时读提示词的地方各不相同

同一条规则,三处措辞落地:

规则04 章:本包 prompts02 章:MCP 工具 description05 章:skill / rule 自然语言
载体AGENT_PROMPT + 工具 description(src/prompts/system.ts)registerTool 的 description(packages/mcp/src/index.ts:140,227)SKILL.md 的 Workflow / c7-docs.md 步骤
先 resolve 再 query「Step 1 ALWAYS ... resolveLibraryId」「You MUST call this function before 'Query Documentation'」(:140)「1. Resolve the library ID. 2. Query the docs.」
库 ID 格式description 内联示例「format: /org/project」(mcp index.ts:143,239)「their Context7 IDs (/org/project format)」
每问 ≤ 3 次「more than 3 times per question」「Do not call this tool more than 3 times」(:170,229)「Do not call either tool more than 3 times per question」
单概念一次查询query 参数 .describe()(query-docs.ts:45-49)参数 description(mcp index.ts:239)「scoped to a single concept ... separate call per concept」
已给 ID 可跳过 resolve「UNLESS the user explicitly provides ...」同措辞(:140,227)「If the user supplies a library ID ... skip step 1」

这份契约是被「同步」维护的,不是巧合。 pi 包在自己的 prompts 文件顶部写了一行注释,把这件事挑明:

// packages/pi/lib/prompts.ts:1-4
// Tool titles, descriptions, and parameter descriptions are copied verbatim
// from @upstash/context7-mcp (packages/mcp/src/index.ts) so pi and MCP clients
// give the LLM identical instructions. Update both together when tweaking guidance.

一句话:「先 resolve 再 query、每问≤3 次、库 ID=/org/project、单概念一次查询」是 Context7 的产品级规则,提示词只是它在某个载体上的一次投影。 想改行为,得同时改所有投影(见 05 章讲的「分发即产品」)。

小注意: 三处措辞并非逐字相同。本包 AGENT_PROMPT 是为 AI SDK 场景重写的四步版;MCP description 更长(带完整字段清单);skill 是给「宿主 Agent」读的散文。契约一致,措辞随载体裁剪——这正是提示词工程的手艺所在。


5. pi:pi.dev 上的一层极薄封装

packages/pi 是把同两个工具接进 pi.dev 编码 Agent 的扩展,整个入口只有注册两行——pi.registerTool(resolveLibraryIdTool) / pi.registerTool(queryDocsTool)(packages/pi/extensions/context7.ts:5-8);它不复用本包,而是用 typebox 写自己的工具、并把工具描述逐字抄自 MCP(见上一节注释),是「同一份工作流契约 + 又一种载体」的最小示例。


6. 边界与局限

  • 工具本身不判「够不够好」。 「≤3 次就收手」「选最佳库」全靠模型读提示词自觉;代码层只有 stepCountIs 一道硬刹车,数的是总步数而非每工具调用数——真要让某工具刚好卡在 3 次,提示词是唯一防线。
  • 无重试/无缓存/无并发控制。 execute 出错就返回一句错误字符串,交给模型自己决定要不要再试;这一层不做退避或去重。
  • 提示词是「建议」不是「强制」。 模型可以违背 AGENT_PROMPT(比如跳过 resolve);包不做运行时校验来拦截违规调用。
  • 依赖较新的 AI SDK。 peerDependency 要求 ai >= 6.0.0zod >= 4.0.0(packages/tools-ai-sdk/package.json:31-35),ToolLoopAgent / stepCountIs 是 v6 的 API。
  • ./agent 单独出口。 除主入口外还构建了 ./agent 子出口(tsup.config.ts:4-7),让只想要 Agent 的人少打包工具代码。

7. 横向对比(同库兄弟章)

  • 想知道这两个工具底层怎么跟后端对话(HTTP、认证、/org/project 从哪来)→ 01 章 SDK 与 API 契约
  • 想看同一套工具在 MCP 服务器里怎么暴露、传输与安全怎么做、tool description 的完整版长什么样 → 02 章 MCP 服务器
  • 想要命令行里直接查文档/一键接入 → 03 章 ctx7 CLI
  • 想理解这份「工作流契约」如何以 Skills / Plugins / Rules 的形态分发到各家 Agent → 05 章 分发即产品
  • 全景与阅读地图 → index

8. 代码地图(导航索引)

主题文件路径符号名
统一出口(工具/Agent/prompts + SDK 类型)packages/tools-ai-sdk/src/index.tsresolveLibraryId queryDocs Context7Agent SYSTEM_PROMPT AGENT_PROMPT
resolve 工具(tool() 包装)packages/tools-ai-sdk/src/tools/resolve-library-id.ts:33resolveLibraryId
query 工具(tool() 包装)packages/tools-ai-sdk/src/tools/query-docs.ts:33queryDocs
工具配置类型packages/tools-ai-sdk/src/tools/types.ts:4Context7ToolsConfig
多步 Agentpackages/tools-ai-sdk/src/agents/context7.ts:38Context7Agent / stepCountIs
强约束工作流提示词packages/tools-ai-sdk/src/prompts/system.ts:20AGENT_PROMPT
精简版系统提示词packages/tools-ai-sdk/src/prompts/system.ts:8SYSTEM_PROMPT
工具 description(契约投影)packages/tools-ai-sdk/src/prompts/system.ts:52,78RESOLVE_LIBRARY_ID_DESCRIPTION / QUERY_DOCS_DESCRIPTION
pi 薄封装入口packages/pi/extensions/context7.ts:5context7
pi「逐字抄自 MCP」注释packages/pi/lib/prompts.ts:1(文件头注释)
契约的 MCP 投影(对比用)packages/mcp/src/index.ts:140,227tool description