4.3 案例 2:开发 AI 搜索智能体
在上节内容,我们学习了如何开发对话类的 MCP 客户端,调用用户配置的 MCP 服务器与大模型交互。
在本节内容,我们继续学习如何开发智能体类的 MCP 客户端。
在开始讲解案例之前,先来介绍一下我去年做的一款 AI 搜索引擎产品:ThinkAny。
2024 年 3 月,我发布了 ThinkAny。其核心功能是根据用户输入的问题,先联网检索信息,再请求大模型回答用户的问题。核心原理是通过 RAG 技术,使用传统搜索引擎的搜索结果作为上下文补充给大模型,减少大模型的幻觉,给到用户更准确的回答。
ThinkAny 的一个问答示例,如图 4-11 所示:

本节内容,我们计划开发一个与 ThinkAny 同名的 AI 搜索智能体,通过这个案例来演示智能体类 MCP 客户端开发的具体流程。
4.3.1 开发目标
我梳理了 AI 搜索引擎类产品的核心流程,如图 4-12 所示:

图 4-12 AI 搜索引擎类产品的核心流程
前面提到,此类产品背后的核心技术原理是 RAG(Retrieval-Augmented Generation,检索增强生成),其中的检索步骤,通过调用传统搜索引擎查询接口来实现。为了得到更丰富的参考内容,一般需要对用户的原始查询问题进行改写,并对检索得到的部分网页读取详细内容。
ThinkAny 网页版完整实现了上述流程。鉴于篇幅有限,本节内容讲解 AI 搜索智能体开发时,只聚焦实现三个核心步骤,即:
- 问题改写:Query Rewrite
- 联网检索:Retrieve
- 读取网页内容:Read Content
以上三个步骤都使用 MCP 服务器提供的工具来实现,由 AI 搜索智能体内置 MCP 服务器配置,通过提示词编排工作流,实现基本的 AI 搜索问答功能。
4.3.2 前置准备
我们先来为开发目标中所述的三个步骤选择合适的 MCP 服务器并进行调试。
- 获取问题改写 MCP 服务器工具列表
用户输入的查询问题,可能是某个关键词,或者某个句子。为了有更好的搜索结果,往往需要对用户的原始查询问题进行改写,然后用改写后的子查询联网检索内容。
这里选择一个叫做 query-rewrite-mcp 的 MCP 服务器,其提供一个 rewrite_query 工具,可以对用户输入的查询问题进行改写。
配置此 MCP 服务器的运行信息,通过 listTools 函数获取此 MCP 服务器内部定义的工具列表,调试输出结果,如图 4-13 所示:

图 4-13 调试 query-rewrite-mcp 工具列表
调试逻辑里面用到的
listTools函数,在 4.2 节已详细讲解其实现逻辑,本节内容不再赘述。调试此 MCP 服务器需要填写的OPENROUTER_API_KEY在 OpenRouter 平台获取;填写的REWRITE_MODEL是用于改写问题的模型,开发者可自行指定,但推荐使用claude系列模型,会有更好的改写效果。
- 调用问题改写工具
根据问题改写 MCP 服务器输出的工具列表,通过 callTool 函数调用 rewrite_query 工具,调试输出结果,如图 4-14 所示:

图 4-14 调试 rewrite_query 工具
调试逻辑里面用到的
callTool函数,在 4.2 节已详细讲解其实现逻辑,本节内容不再赘述。
通过上述的调试输出可以得知,rewrite_query 工具可以改写用户的原始查询问题,返回更加丰富的改写内容。比如输入的原始查询问题是:
{ "query": "MCP 是什么?"}改写后的内容是:
{ "questions": ["MCP 是什么?", "MCP 代表什么?", "MCP 的定义和用途是什么?"], "concepts": ["MCP"], "queries": ["MCP 定义", "MCP 含义", "MCP 用途"]}- 获取联网检索 MCP 服务器工具列表
这里选择一个叫做 serper-mcp 的 MCP 服务器,其提供一个 google_search 工具,可以模拟 Google 搜索引擎查询得到搜索结果。
配置此 MCP 服务器的运行信息,通过 listTools 函数获取此 MCP 服务器内部定义的工具列表,调试输出结果,如图 4-15 所示:

图 4-15 调试 serper-mcp 工具列表
调试此 MCP 服务器需要填写的
SERPER_API_KEY在 Serper 平台获取。
- 调用联网检索工具
根据联网检索 MCP 服务器输出的工具列表,通过 callTool 函数调用 google_search 工具,调试输出结果,如图 4-16 所示:

图 4-16 调试 google_search 工具
通过上述的调试输出得知,google_search 工具可以模拟 Google 搜索引擎查询得到搜索结果。比如输入的查询参数为:
{ "q": "mcp"}此工具返回搜索结果列表,每个结果包含标题、链接、描述、来源等信息。其中的一条搜索结果示例:
{ "title": "Model Context Protocol: Introduction", "link": "https://modelcontextprotocol.io/introduction", "snippet": "MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications."}- 获取读取网页内容 MCP 服务器工具列表
这里选择一个叫做 jina-mcp 的 MCP 服务器,其提供一个 jina_reader 工具,可以读取网页内容。配置此 MCP 服务器的运行信息,通过 listTools 函数获取此 MCP 服务器内部定义的工具列表,调试输出结果,如图 4-17 所示:

图 4-17 调试 jina-mcp 工具列表
调试此 MCP 服务器需要填写的
JINA_API_KEY在 Jina 平台获取。
- 调用读取网页内容工具
根据读取网页内容 MCP 服务器输出的工具列表,通过 callTool 函数调用 jina_reader 工具,调试输出结果,如图 4-18 所示:

从调试结果得知,此工具接收网页 URL 地址,默认通过 Markdown 格式返回读取到的网页内容。
至此,我们已经准备好了实现 AI 搜索智能体需要用到的全部 MCP 服务器及其内部定义的工具。接下来就可以专注实现 AI 搜索智能体与大模型交互的逻辑了。
4.3.3 定义 MCP 服务器配置
跟上一个案例从配置文件读取用户配置的 MCP 服务器不同,此案例中,AI 搜索智能体需要用到的 MCP 服务器和工具是固定的,因此可以在程序逻辑中直接定义。MCP 服务器配置内容如下:
{ "mcpServers": { "query-rewrite-mcp": { "command": "npx", "args": ["query-rewrite-mcp"], "env": { "OPENROUTER_API_KEY": "xxx", "REWRITE_MODEL": "anthropic/claude-3.7-sonnet" } }, "serper-search-mcp": { "command": "uvx", "args": ["serper-mcp-server"], "env": { "SERPER_API_KEY": "xxx" } }, "jina-reader-mcp": { "command": "npx", "args": ["jina-mcp-tools"], "env": { "JINA_API_KEY": "xxx" } } }}配置内容做了脱敏处理,每个 MCP 服务器用到的参数使用了占位符。开发者可根据实际情况自行填写。
4.3.4 过滤 MCP 服务器工具列表
跟上一个案例把用户配置的 MCP 服务器内部定义的全部工具传给大模型不同,此案例中,因为用到的工具是固定的,因此可以定义一个工具列表,对获取到的 MCP 服务器工具列表进行过滤,只选择实现 AI 搜索智能体需要用到的工具。
过滤 MCP 服务器工具列表的逻辑如下:
const mcpServers = await getMcpServers(config);const mcpTools = await getMcpTools(mcpServers);
const filterTools = ["rewrite_query", "google_search", "jina_reader"];const filteredTools = mcpTools.filter((tool) => filterTools.includes(tool.name));此逻辑中,
config是在程序中定义的 MCP 服务器配置内容。getMcpServers和getMcpTools函数在 4.2 节已详细讲解其实现逻辑,本节内容不再赘述。
调用接口获取过滤后的工具列表,调试输出结果,如图 4-19 所示:

图 4-19 获取过滤 MCP 服务器工具列表
从调试结果得到,在接口逻辑中获得了 AI 搜索智能体需要用到的三个工具及其对应的 MCP 服务器:
rewrite_query工具,对应query-rewrite-mcpMCP 服务器google_search工具,对应serper-search-mcpMCP 服务器jina_reader工具,对应jina-reader-mcpMCP 服务器
4.3.5 通过提示词编排工作流
为了让大模型按照 AI 搜索引擎的标准流程调度工具,我们需要设置系统提示词,编排工作流。
本案例设计的系统提示词如下:
你是 ThinkAny,由 ThinkAny AI 开发的搜索智能体。
# 核心工作流
## 执行原则
- 检查上下文中已有的工具执行结果,避免重复调用- 按顺序执行:问题改写 → 信息检索 → 内容获取 → 生成答案- 一次只调用一个工具,等待结果后再继续
## 阶段一:问题改写
**条件**:仅当上下文中无 rewrite_query 结果时执行**工具**:rewrite_query**目标**:将用户查询转换为结构化搜索查询
- 生成具体问题列表 (questions)- 提取核心概念 (concepts)- 优化搜索查询 (queries)
## 阶段二:信息检索
**工具**:google_search(可多次调用,最多 3 次)**策略**:
- 优先使用 rewrite_query 结果中的 queries 和 concepts- 每次搜索 10 条记录,总计不超过 30 条- 按相关性排序,优选权威网站
## 阶段三:内容获取
**工具**:jina_reader(可多次调用)**要求**:
- 使用 "Markdown" 格式获取内容- 优先读取最相关的 URL- 过滤无效内容(验证页面、错误页面等)- 确保获得 2-3 个网页的详细内容
## 最终答案生成
**要求**:
- 直接回答用户原始问题- 使用 [1]、[2] 等数字标注引用- 末尾提供完整引用链接列表- 使用清晰的 markdown 格式
# 工具调用格式
<<tool-start>>{"server_name": "工具服务器名称","tool_name": "工具名称","tool_params": {"参数名": "参数值"}}<<tool-end>>
# 执行参数
- 用户查询:USER_QUERY={USER_QUERY}- 对话上下文:CONTEXT_MESSAGES={CONTEXT_MESSAGES}- 工具执行结果:PREVIOUS_TOOL_RESULTS={PREVIOUS_TOOL_RESULTS}- 可用工具列表:AVAILABLE_TOOLS={AVAILABLE_TOOLS}此提示词的核心思路是定义了三个步骤,每个步骤包含一个可以调用的工具。让大模型根据定义编排工作流,返回每个步骤对应的工具调用信息。
4.3.6 实现与大模型交互逻辑
跟上一个案例与大模型交互的逻辑类似,AI 搜索智能体与大模型交互的步骤也包括:
- 请求大模型挑选工具
- 解析大模型响应的工具信息
- 调用工具
- 重复步骤 1-3,直到大模型不再返回工具调用信息或者达到最大循环次数
在上一个案例已详细讲解以上步骤的具体实现逻辑,本节内容不再赘述。而是直接给出 AI 搜索智能体与大模型交互的逻辑实现代码,如下所示:
export async function POST(req: Request) { const { query } = await req.json();
const mcpPrompt = await fs.readFile( path.join(process.cwd(), "app/api/mcp/test/search.md"), "utf-8" );
const mcpServers = await getMcpServers(config); const mcpTools = await getMcpTools(mcpServers);
const filterTools = ["rewrite_query", "google_search", "jina_reader"]; const filteredTools = mcpTools.filter((tool) => filterTools.includes(tool.name) );
let contextMessages: MixContent[] = []; let toolResults = ""; let reply = "";
// loop for max 10 times for (let i = 0; i < 10; i++) { // pick tool const pickToolResult = await chatWithLLM({ mcpPrompt, query, contextMessages: JSON.stringify(contextMessages), tools: JSON.stringify(filteredTools), toolResults: toolResults, });
// parse content let content = ""; for await (const chunk of pickToolResult.textStream) { content += chunk; } const mixContents = parseMixContents(content); contextMessages.push(...mixContents); reply += content;
// parse tool let callToolParams = null; for (const mixContent of mixContents) { if (mixContent.type === "tool") { const tool = mixContent.tool; if ( tool && tool.tool_name && tool.server_name && mcpServers[tool.server_name] && mcpServers[tool.server_name].command ) { callToolParams = { command: mcpServers[tool.server_name].command, args: mcpServers[tool.server_name].args || [], env: mcpServers[tool.server_name].env || {}, name: tool.tool_name, params: tool.tool_params, }; break; } } }
// need to call tool if (callToolParams) { const callToolResult = await callTool(callToolParams); toolResults = JSON.stringify(callToolResult);
reply += `\n\n${toolResults}\n\n`;
continue; }
// no need to call tool, end loop break; }
return new Response(reply);}跟 AI 对话客户端与大模型交互的逻辑比较,AI 搜索智能体与大模型交互的逻辑主要是传递的工具列表以及系统提示词不同,其他步骤基本类似。
4.3.7 调试接口逻辑
对上一步骤实现的 AI 搜索智能体接口进行调试,输入查询问题,输出调试结果,如图 4-20 所示:

图 4-20 调试 AI 搜索智能体接口,查看工具调用信息
可以看到,大模型按照提示词约定的工作流,依次调用了三个工具,最终输出的回复内容,如图 4-21 所示:

图 4-21 调试 AI 搜索智能体接口,查看回复结果
通过最终的回复结果可以看到,跟 ThinkAny 网页版得到的答案基本一致。
至此,我们完成了 AI 搜索智能体服务端逻辑的开发。实现了 AI 搜索引擎类产品的核心功能。
4.3.8 在 Cursor 中测试工具调用
在前面的内容中,我们讲解了 AI 搜索智能体服务端逻辑的主要实现,通过调试接口,得到了最终的回复内容。但是因为缺了 UI 层,无法直观的展示 AI 搜索智能体的工作流程。
我们可以选择在 Cursor 中继续调试,在 Cursor 的 MCP 配置文件中写入 AI 搜索智能体内置的 MCP 服务器配置。使用 AI 搜索智能体内置的系统提示词 search.md,发送同一个查询问题,查看工具调用信息,如图 4-22 所示:

图 4-22 在 Cursor 中测试工具调用
可以看到,Cursor 作为 MCP 客户端,按照提示词约定的工作流与大模型交互,先进行了一次问题改写,再进行了三次联网检索,最后读取了两个网页内容,最终回复查询问题,给出了准确的回答。
去掉系统提示词,再用同一个查询问题测试,可以看到,Cursor 没有调用问题改写和获取网页内容的 MCP 服务器,最终回复的内容准确度不足。如图 4-23 所示:

图 4-23 在 Cursor 中测试无系统提示词场景的工具调用
在 Cursor 中验证了提示词和工具的有效性,跟本节内容讲解的 AI 搜索智能体实现的功能一致。
4.3.9 小结
本节内容通过一个 AI 搜索智能体的例子,讲解了开发智能体类 MCP 客户端的基本流程和核心步骤。
理解了本节内容,就可以开始动手开发自己的 AI 智能体了。