插件系统实现:动态库加载 + 统一 Plugin 接口 + 工具回调调用
简历原文:(5)插件系统实现:(1)通过动态加载动态库(支持.dll(Windows)、.so(Linux)、.dylib(macOS)),并通过统一的Plugin抽象接口,管理tools/Prompts/Resources三类插件,使新增功能可以以"插件"形式独立开发和部署,而无需修改或重新编译主服务(2)调用:通过客户端发起的不同method的请求,以及根据mcp协议的工具描述字段,查找对应的工具回调执行,进行结果返回
一、架构总览:插件系统在整个 MCP Server 中的位置
┌───────────────────────────────────────────────────────────┐
│ MCP Server 主进程 │
│ │
│ main.cpp │
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
│ │ PluginsLoader │ │ Server │ │
│ │ │ │ │ │
│ │ LoadPlugins() │ │ functionMap │ │
│ │ ├── dlopen() │ │ ├── "tools/list" ──────┐ │ │
│ │ ├── dlsym() │ │ ├── "tools/call" ────┐ │ │ │
│ │ └── CreatePlugin() │ │ ├── "prompts/list" │ │ │ │
│ │ │ │ ├── "prompts/get" │ │ │ │
│ │ GetPlugins() ──────┼──►│ ├── "resources/list" │ │ │ │
│ │ │ │ └── "resources/read" │ │ │ │
│ └─────────────────────┘ └───────────────────┼──┼─┘ │
│ │ │ │
│ OverrideCallback: 用闭包捕获 loader ───────────┘ │ │
│ 遍历插件 → 匹配工具名 → HandleRequest() ─────────┘ │
│ │
└───────────────────┬───────────────────────────────────────┘
│ dlopen / dlsym
┌───────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ calculator│ │ weather │ ... │notification│
│ .dylib │ │ .dylib │ │ .dylib │
│ │ │ │ │ │
│ PluginAPI │ │ PluginAPI│ │ PluginAPI │
│ (C ABI) │ │ (C ABI) │ │ (C ABI) │
└──────────┘ └──────────┘ └──────────┘
独立编译 独立编译 独立编译
核心设计思想:插件以动态库(.so/.dylib/.dll)形式独立编译,通过 C ABI 导出统一的 PluginAPI 函数指针表,主服务在运行时通过 dlopen/dlsym 加载并注册到 Server 的路由表,实现零耦合扩展。
二、PluginAPI:统一的 C ABI 插件接口
2.1 为什么用 C ABI 而不是 C++ 虚函数
插件和主服务是分别编译 的独立二进制(.so/.dylib),可能使用不同版本的编译器。C++ 的 vtable 布局、name mangling 规则在不同编译器之间不保证兼容,而 C 函数的调用约定(ABI)在同一平台上是统一的。
| C++ 虚函数 | C ABI 函数指针 | |
|---|---|---|
| 符号名 | _ZN2vx3mcp12CreatePluginEv(编译器 mangling) |
CreatePlugin(固定) |
| vtable 布局 | 编译器决定,不同编译器可能不同 | 不依赖 vtable |
| 跨编译器兼容 | ❌ | ✅ |
| dlsym 查找 | ❌ 需要知道 mangled name | ✅ dlsym(handle, "CreatePlugin") |
2.2 接口定义
cpp
// 跨平台导出宏
#ifdef _WIN32
#define PLUGIN_API __declspec(dllexport)
#else
#define PLUGIN_API __attribute__((visibility("default")))
#endif
// ★ extern "C" 包裹 ------ 告诉编译器使用 C ABI,禁止 name mangling
#ifdef __cplusplus
extern "C" {
#endif
三类插件元数据结构体:
cpp
// ① 工具(Tools):可执行的函数/计算
typedef struct {
const char* name; // 工具名,如 "calculator"
const char* description; // 工具描述
const char* inputSchema; // JSON Schema 字符串,定义输入参数格式
} PluginTool;
// ② 提示(Prompts):预定义的 LLM 提示模板
typedef struct {
const char* name;
const char* description;
const char* arguments; // JSON 参数定义
} PluginPrompt;
// ③ 资源(Resources):可读取的数据源
typedef struct {
const char* name;
const char* description;
const char* uri; // 资源 URI,如 "file:///config.json"
const char* mime; // MIME 类型,如 "application/json"
} PluginResource;
插件类型枚举:
cpp
typedef enum {
PLUGIN_TYPE_TOOLS = 0, // 工具类插件
PLUGIN_TYPE_PROMPTS = 1, // 提示类插件
PLUGIN_TYPE_RESOURCES = 2 // 资源类插件
} PluginType;
通知系统:
cpp
// 插件 → 主服务 → 客户端 的通知回调
typedef void (*ClientNotificationCallback)(const char* pluginName, const char* notification);
typedef struct {
ClientNotificationCallback SendToClient; // 由主服务注入
} NotificationSystem;
核心:PluginAPI 函数指针表:
cpp
typedef struct {
// ── 身份信息 ──
const char* (*GetName)(); // 插件名称
const char* (*GetVersion)(); // 插件版本
// ── 类型与生命周期 ──
PluginType (*GetType)(); // 返回 TOOLS / PROMPTS / RESOURCES
int (*Initialize)(); // 初始化,返回 1 = 成功
void (*Shutdown)(); // 清理资源
// ── 核心处理 ──
char* (*HandleRequest)(const char* request); // ★ 处理 JSON-RPC 请求
// ── 工具元数据 ──
int (*GetToolCount)(); // 工具数量
const PluginTool* (*GetTool)(int index); // 获取第 i 个工具定义
// ── 提示元数据 ──
int (*GetPromptCount)();
const PluginPrompt* (*GetPrompt)(int index);
// ── 资源元数据 ──
int (*GetResourceCount)();
const PluginResource* (*GetResource)(int index);
// ── 通知系统(由主服务注入) ──
NotificationSystem* notifications;
} PluginAPI;
两个导出函数(每个插件 .so 必须导出):
cpp
PLUGIN_API PluginAPI* CreatePlugin(); // 创建插件实例
PLUGIN_API void DestroyPlugin(PluginAPI*); // 销毁插件实例
#ifdef __cplusplus
} // extern "C" 结束
#endif
设计理念 :PluginAPI 本质上是一个手写的 vtable 。每个函数指针相当于一个虚函数,但内存布局由你自己控制,不依赖编译器。CreatePlugin / DestroyPlugin 对应工厂方法。
2.3 三类插件的区别
| 类型 | GetType() 返回值 | 核心元数据 | 典型场景 |
|---|---|---|---|
| Tools | PLUGIN_TYPE_TOOLS |
PluginTool(name + inputSchema) |
计算器、天气查询、代码审查 |
| Prompts | PLUGIN_TYPE_PROMPTS |
PluginPrompt(name + arguments) |
LLM 提示模板 |
| Resources | PLUGIN_TYPE_RESOURCES |
PluginResource(uri + mime) |
文件、数据库、配置读取 |
三类共享同一个 HandleRequest() 入口,通过 GetType() 区分类型,通过不同的 GetXxxCount/GetXxx 方法暴露元数据。
三、PluginsLoader:动态库加载器
3.1 跨平台类型定义
cpp
// 跨平台动态库句柄类型
#ifdef _WIN32
#include <windows.h>
typedef HMODULE LibraryHandle; // Windows: HMODULE
#else
#include <dlfcn.h>
typedef void* LibraryHandle; // Linux/macOS: void* (dlopen 的返回值)
#endif
3.2 PluginEntry:加载后的插件描述
cpp
struct PluginEntry {
std::string path; // .so/.dylib/.dll 文件路径
LibraryHandle handle; // 动态库句柄(dlopen 返回的)
PluginAPI* instance; // CreatePlugin() 返回的插件实例
// 从动态库中查找到的两个函数指针
PluginAPI* (*createFunc)(); // → CreatePlugin
void (*destroyFunc)(PluginAPI*);// → DestroyPlugin
};
3.3 PluginsLoader 类
cpp
class PluginsLoader {
public:
PluginsLoader();
~PluginsLoader(); // 析构时自动 UnloadPlugins()
bool LoadPlugins(const std::string& directory); // 扫描目录加载所有插件
void UnloadPlugins(); // 卸载所有插件
const std::vector<PluginEntry>& GetPlugins() const; // 获取已加载插件列表
private:
bool LoadPlugin(const std::string& path); // 加载单个插件
void UnloadPlugin(PluginEntry& entry); // 卸载单个插件
std::vector<PluginEntry> m_plugins; // 已加载的插件列表
};
3.4 LoadPlugins:扫描目录递归查找动态库
cpp
bool PluginsLoader::LoadPlugins(const std::string& directory) {
try {
// ★ 递归遍历目录下所有文件
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (entry.is_regular_file()) {
std::string extension = entry.path().extension().string();
// 根据平台检查文件后缀
#ifdef _WIN32
if (extension == ".dll")
#else
#ifdef __APPLE__
if (extension == ".dylib" || extension == ".so") // macOS 支持两种
#else
if (extension == ".so") // Linux
#endif
#endif
{
LoadPlugin(entry.path().string());
}
}
}
return true;
} catch (const std::exception& ex) {
LOG(ERROR) << "Error loading plugins: " << ex.what();
return false;
}
}
关键 :使用 C++17 的 std::filesystem::recursive_directory_iterator 递归扫描整个插件目录,自动发现所有 .so / .dylib / .dll 文件。
3.5 LoadPlugin:加载单个插件的完整流程(核心)
cpp
bool PluginsLoader::LoadPlugin(const std::string& path) {
PluginEntry entry;
entry.path = path;
// ═══════════════════════════════════════════════
// 第 ① 步:dlopen --- 打开动态库文件
// ═══════════════════════════════════════════════
#ifdef _WIN32
entry.handle = LoadLibraryA(path.c_str());
if (!entry.handle) { /* 错误处理 */ return false; }
#else
entry.handle = dlopen(path.c_str(), RTLD_LAZY);
// ^^^^^^^^
// RTLD_LAZY: 延迟绑定 --- 只在函数第一次被调用时才解析符号
// (相比 RTLD_NOW 更快,但可能在运行时才发现符号缺失)
if (!entry.handle) {
LOG(ERROR) << "Failed to load plugin: " << path << " - " << dlerror();
return false;
}
#endif
// ═══════════════════════════════════════════════
// 第 ② 步:dlsym --- 查找导出符号
// ═══════════════════════════════════════════════
#ifdef _WIN32
entry.createFunc = (PluginAPI*(*)()) GetProcAddress(entry.handle, "CreatePlugin");
entry.destroyFunc = (void(*)(PluginAPI*)) GetProcAddress(entry.handle, "DestroyPlugin");
#else
entry.createFunc = (PluginAPI*(*)()) dlsym(entry.handle, "CreatePlugin");
entry.destroyFunc = (void(*)(PluginAPI*)) dlsym(entry.handle, "DestroyPlugin");
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// dlsym 在动态库的符号表中查找名为 "CreatePlugin" 的函数
// 因为 extern "C",符号名就是 "CreatePlugin"(无 mangling)
// 返回 void*,需要强转为正确的函数指针类型
#endif
// ═══════════════════════════════════════════════
// 第 ③ 步:检查必要函数是否存在
// ═══════════════════════════════════════════════
if (!entry.createFunc || !entry.destroyFunc) {
LOG(ERROR) << "Plugin does not export required functions: " << path;
dlclose(entry.handle); // 关闭无效的动态库
return false;
}
// ═══════════════════════════════════════════════
// 第 ④ 步:调用 CreatePlugin() --- 获取 PluginAPI 实例
// ═══════════════════════════════════════════════
entry.instance = entry.createFunc();
// ^^^^^^^^^^^^^^^^^^^^^
// 通过函数指针调用插件的 CreatePlugin()
// 返回一个 PluginAPI* ------ 包含所有函数指针的结构体
// ═══════════════════════════════════════════════
// 第 ⑤ 步:调用 Initialize() --- 初始化插件
// ═══════════════════════════════════════════════
if (!entry.instance->Initialize()) {
LOG(ERROR) << "Plugin initialization failed: " << path;
entry.destroyFunc(entry.instance); // 销毁失败的插件
dlclose(entry.handle);
return false;
}
// ═══════════════════════════════════════════════
// 第 ⑥ 步:加入插件列表
// ═══════════════════════════════════════════════
m_plugins.push_back(entry);
LOG(INFO) << "Loaded plugin: " << entry.instance->GetName()
<< " v" << entry.instance->GetVersion();
return true;
}
整个流程可以总结为一条加载链:
dlopen("calculator.dylib")
→ dlsym(handle, "CreatePlugin") → 拿到函数指针
→ CreatePlugin() → 拿到 PluginAPI* 实例
→ instance->Initialize() → 初始化
→ m_plugins.push_back(entry) → 保存到列表
3.6 UnloadPlugin:卸载流程(完全逆序)
cpp
void PluginsLoader::UnloadPlugin(PluginEntry& entry) {
if (entry.instance) {
entry.instance->Shutdown(); // ① 调用插件的清理方法
entry.destroyFunc(entry.instance); // ② 调用 DestroyPlugin 销毁实例
entry.instance = nullptr;
}
if (entry.handle) {
dlclose(entry.handle); // ③ 关闭动态库,释放内存映射
entry.handle = nullptr;
}
}
析构器自动清理:
cpp
PluginsLoader::~PluginsLoader() {
UnloadPlugins(); // 遍历所有插件逐一卸载
}
四、Calculator 插件示例:一个完整的 Tools 类插件
4.1 工具定义数组
一个 Tools 类插件可以包含多个工具。Calculator 插件定义了 8 个工具:
cpp
static PluginTool methods[] = {
{"calculator", "Evaluates a mathematical expression",
R"({"type":"object","properties":{"expression":{"type":"string","description":"Math expression"}},"required":["expression"]})"},
{"add", "Adds two numbers",
R"({"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"]})"},
{"subtract", "Subtracts b from a",
R"({"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"]})"},
{"multiply", "Multiplies two numbers", /* ... */ },
{"divide", "Divides a by b", /* ... */ },
{"power", "Raises base to exponent", /* ... */ },
{"sqrt", "Square root of a number", /* ... */ },
{"factorial", "Factorial of n (0-20)", /* ... */ }
};
每个 PluginTool 有三个字段:
name:工具名(客户端用这个名字调用)description:自然语言描述(供 LLM 理解工具用途,RAG-MCP 用这个字段做 Embedding 匹配)inputSchema:JSON Schema 字符串,定义输入参数的类型和约束
4.2 身份函数实现
cpp
const char* GetNameImpl() { return "calculator-tools"; }
const char* GetVersionImpl() { return "1.0.0"; }
PluginType GetTypeImpl() { return PLUGIN_TYPE_TOOLS; } // 声明自己是 Tools 类型
int InitializeImpl() { return 1; } // 初始化成功返回 1(true)
void ShutdownImpl() {} // 无资源需要清理
4.3 元数据查询
cpp
int GetToolCountImpl() {
return sizeof(methods) / sizeof(methods[0]); // = 8,编译期确定
}
const PluginTool* GetToolImpl(int index) {
if (index < 0 || index >= GetToolCountImpl()) return nullptr;
return &methods[index]; // 返回第 index 个工具的指针
}
主服务通过 GetToolCount() + GetTool(i) 遍历所有工具,获取名称、描述和参数 Schema------这些信息在 tools/list 响应中返回给客户端。
4.4 HandleRequest:核心请求处理
cpp
char* HandleRequestImpl(const char* req) {
json response;
response["content"] = json::array();
response["isError"] = false;
try {
auto request = json::parse(req);
// ★ 从 JSON-RPC 请求中提取工具名和参数
std::string toolName = request["params"]["name"].get<std::string>();
auto args = request["params"]["arguments"];
std::string resultText;
double result = 0;
// ★ 根据 toolName 分派到不同的处理逻辑
if (toolName == "calculator") {
std::string expr = args["expression"].get<std::string>();
result = ExpressionParser::evaluate(expr); // 表达式解析器
resultText = expr + " = " + std::to_string(result);
}
else if (toolName == "add") {
double a = args["a"].get<double>();
double b = args["b"].get<double>();
result = a + b;
resultText = std::to_string(a) + " + " + std::to_string(b) + " = " + std::to_string(result);
}
else if (toolName == "subtract") { /* 类似逻辑 */ }
else if (toolName == "multiply") { /* 类似逻辑 */ }
else if (toolName == "divide") {
double a = args["a"].get<double>();
double b = args["b"].get<double>();
if (b == 0) throw std::runtime_error("Division by zero");
result = a / b;
resultText = std::to_string(a) + " / " + std::to_string(b) + " = " + std::to_string(result);
}
// ... power, sqrt, factorial 类似
// ★ 构建 MCP 协议格式的响应
json content;
content["type"] = "text";
content["text"] = resultText;
response["content"].push_back(content);
} catch (const std::exception& e) {
response["isError"] = true;
json errorContent;
errorContent["type"] = "text";
errorContent["text"] = std::string("Error: ") + e.what();
response["content"].push_back(errorContent);
}
// ★ 返回堆分配的 C 字符串(调用方负责 delete[])
std::string resultStr = response.dump();
char* buffer = new char[resultStr.length() + 1];
strcpy(buffer, resultStr.c_str());
return buffer;
}
注意内存管理 :HandleRequest 返回 char*(堆分配)。因为跨动态库边界传递 std::string 不安全(不同编译器的 std::string 实现可能不同),所以用 C 字符串。调用方(主服务)负责 delete[] 释放。
4.5 组装 PluginAPI 结构体 & 导出
cpp
// 将所有函数指针打包到一个 PluginAPI 结构体中
static PluginAPI plugin = {
GetNameImpl, // GetName
GetVersionImpl, // GetVersion
GetTypeImpl, // GetType
InitializeImpl, // Initialize
HandleRequestImpl, // HandleRequest
ShutdownImpl, // Shutdown
GetToolCountImpl, // GetToolCount
GetToolImpl, // GetTool
nullptr, // GetPromptCount(Tools 类型不需要)
nullptr, // GetPrompt
nullptr, // GetResourceCount
nullptr // GetResource
};
// ★ extern "C" + PLUGIN_API 导出两个工厂函数
extern "C" PLUGIN_API PluginAPI* CreatePlugin() { return &plugin; }
extern "C" PLUGIN_API void DestroyPlugin(PluginAPI*) {} // 静态分配,无需释放
4.6 CMake 编译为动态库
cmake
# ★ SHARED 关键字 --- 编译为动态库(.so / .dylib / .dll)
add_library(calculator SHARED
${PROJECT_SOURCE_DIR}/plugins/calculator/Calculator.cpp
)
# Linux 需要 PIC(位置无关代码),macOS 默认开启
if(UNIX)
set_target_properties(calculator PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()
# 只需要链接线程库,不依赖主服务的任何代码
target_link_libraries(calculator PRIVATE Threads::Threads)
# 头文件路径:只需要知道 PluginAPI.h 的位置
target_include_directories(calculator PRIVATE
${PROJECT_SOURCE_DIR}/include # nlohmann/json
${PROJECT_SOURCE_DIR}/src/interface # PluginAPI.h
)
关键 :add_library(calculator SHARED ...) 决定了这是一个动态库 。在 Linux 上生成 libcalculator.so,macOS 上生成 libcalculator.dylib,Windows 上生成 calculator.dll。
五、主服务如何注册和调用插件
5.1 main.cpp 中的完整流程
第 ① 步:加载所有插件
cpp
auto loader = std::make_shared<vx::mcp::PluginsLoader>();
auto server = std::make_shared<vx::mcp::Server>();
// 从 ./plugins 目录递归扫描并加载所有 .so/.dylib/.dll
if (loader->LoadPlugins(plugins_directory)) {
LOG(INFO) << "Successfully loaded plugins";
}
第 ② 步:注入通知系统
cpp
// 为每个插件创建通知系统,注入主服务的回调函数
for (auto& plugin : loader->GetPlugins()) {
plugin.instance->notifications = new NotificationSystem();
plugin.instance->notifications->SendToClient = ClientNotificationCallbackImpl;
}
这样插件内部就可以调用 notifications->SendToClient("calculator", json) 向客户端推送通知。
第 ③ 步:OverrideCallback 注册工具路由
Server 构造函数中预注册了默认的 tools/list、tools/call 等处理函数(返回空结果),main.cpp 通过 OverrideCallback 用闭包覆盖这些默认处理,在闭包中遍历已加载的插件:
5.2 tools/list --- 列出所有工具
cpp
server->OverrideCallback("tools/list", [&loader](const json& request) {
nlohmann::ordered_json response = MCPBuilder::Response(request);
response["result"]["tools"] = json::array();
// 遍历所有已加载的插件
for (const auto& plugin : loader->GetPlugins()) {
// 只处理 TOOLS 类型的插件
if (plugin.instance->GetType() == PLUGIN_TYPE_TOOLS) {
// 遍历这个插件的所有工具
for (int i = 0; i < plugin.instance->GetToolCount(); i++) {
nlohmann::ordered_json tool;
auto pluginTool = plugin.instance->GetTool(i);
tool["name"] = pluginTool->name;
tool["description"] = pluginTool->description;
tool["inputSchema"] = nlohmann::json::parse(pluginTool->inputSchema);
response["result"]["tools"].push_back(tool);
}
}
}
return response;
});
返回给客户端的 JSON-RPC 响应示例:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "calculator",
"description": "Evaluates a mathematical expression",
"inputSchema": {
"type": "object",
"properties": {
"expression": { "type": "string", "description": "Math expression" }
},
"required": ["expression"]
}
},
{ "name": "add", "description": "Adds two numbers", "inputSchema": { ... } },
{ "name": "get_weather", "description": "Get weather forecast...", "inputSchema": { ... } }
]
}
}
5.3 tools/call --- 调用具体工具(核心!)
cpp
server->OverrideCallback("tools/call", [&loader](const json& request) {
nlohmann::ordered_json response = MCPBuilder::Response(request);
char* res_ptr = nullptr;
// 遍历所有插件
for (const auto& plugin : loader->GetPlugins()) {
if (plugin.instance->GetType() == PLUGIN_TYPE_TOOLS) {
// 遍历这个插件的所有工具
for (int i = 0; i < plugin.instance->GetToolCount(); i++) {
auto pluginTool = plugin.instance->GetTool(i);
// ★ 匹配工具名!request["params"]["name"] == pluginTool->name
if (pluginTool->name == request["params"]["name"]) {
// ★ 调用插件的 HandleRequest,传入完整的 JSON-RPC 请求
res_ptr = plugin.instance->HandleRequest(request.dump().c_str());
if (res_ptr) {
try {
response["result"] = json::parse(res_ptr);
response["result"]["isError"] = false;
} catch (const json::parse_error& e) {
// 插件返回的 JSON 格式有误
response["result"]["isError"] = true;
response["result"]["content"] = json::array();
response["result"]["content"].push_back(
{{"type", "text"}, {"text", "Plugin returned malformed data."}});
}
// ★ 释放插件分配的内存
delete[] res_ptr;
}
return response; // 找到并处理完毕,立即返回
}
}
}
}
return response; // 未找到匹配的工具
});
工具查找流程:
客户端请求: {"method":"tools/call", "params":{"name":"calculator", "arguments":{"expression":"2+3"}}}
│
▼
遍历 loader->GetPlugins()
│
├── plugin[0]: calculator-tools (PLUGIN_TYPE_TOOLS)
│ ├── GetTool(0) → name="calculator" ← ★ 匹配!
│ │ └── HandleRequest(request) → "2+3 = 5"
│ │ └── return response
│ ├── GetTool(1) → name="add"
│ ├── ...
│
├── plugin[1]: weather-tools (PLUGIN_TYPE_TOOLS)
│ ├── GetTool(0) → name="get_weather"
│ ...
5.4 prompts/list & prompts/get --- Prompt 类插件
cpp
server->OverrideCallback("prompts/list", [&loader](const json& request) {
nlohmann::ordered_json response = MCPBuilder::Response(request);
response["result"]["prompts"] = json::array();
for (const auto& plugin : loader->GetPlugins()) {
if (plugin.instance->GetType() == PLUGIN_TYPE_PROMPTS) { // 只看 Prompts 类型
for (int i = 0; i < plugin.instance->GetPromptCount(); i++) {
auto pluginPrompt = plugin.instance->GetPrompt(i);
nlohmann::ordered_json prompt;
prompt["name"] = pluginPrompt->name;
prompt["description"] = pluginPrompt->description;
prompt["arguments"] = nlohmann::json::parse(pluginPrompt->arguments);
response["result"]["prompts"].push_back(prompt);
}
}
}
return response;
});
server->OverrideCallback("prompts/get", [&loader](const json& request) {
// 与 tools/call 类似:遍历 → 匹配名称 → HandleRequest → 返回
for (const auto& plugin : loader->GetPlugins()) {
if (plugin.instance->GetType() == PLUGIN_TYPE_PROMPTS) {
for (int i = 0; i < plugin.instance->GetPromptCount(); i++) {
auto pluginPrompt = plugin.instance->GetPrompt(i);
if (pluginPrompt->name == request["params"]["name"]) {
char* res_ptr = plugin.instance->HandleRequest(request.dump().c_str());
if (res_ptr) {
response["result"] = json::parse(res_ptr);
delete[] res_ptr;
}
return response;
}
}
}
}
return response;
});
5.5 resources/list & resources/read --- Resource 类插件
cpp
server->OverrideCallback("resources/list", [&loader](const json& request) {
// 逻辑同 tools/list,但遍历 PLUGIN_TYPE_RESOURCES
for (const auto& plugin : loader->GetPlugins()) {
if (plugin.instance->GetType() == PLUGIN_TYPE_RESOURCES) {
for (int i = 0; i < plugin.instance->GetResourceCount(); i++) {
auto pluginResource = plugin.instance->GetResource(i);
resource["name"] = pluginResource->name;
resource["uri"] = pluginResource->uri;
resource["mimeType"] = pluginResource->mime;
response["result"]["resources"].push_back(resource);
}
}
}
return response;
});
server->OverrideCallback("resources/read", [&loader](const json& request) {
// 按 URI 匹配(注意:Resources 用 URI 而非 name)
for (const auto& plugin : loader->GetPlugins()) {
if (plugin.instance->GetType() == PLUGIN_TYPE_RESOURCES) {
for (int i = 0; i < plugin.instance->GetResourceCount(); i++) {
auto pluginResource = plugin.instance->GetResource(i);
if (pluginResource->uri == request["params"]["uri"]) { // ★ 按 URI 匹配
char* res_ptr = plugin.instance->HandleRequest(request.dump().c_str());
// ... 解析 & 返回
}
}
}
}
return response;
});
六、Server 路由系统:functionMap + OverrideCallback
6.1 functionMap:方法名 → 处理函数的映射表
cpp
Server::Server() {
functionMap = {
{"initialize", [this](const json& req) { return InitializeCmd(req); }},
{"ping", [this](const json& req) { return PingCmd(req); }},
{"tools/list", [this](const json& req) { return ToolsListCmd(req); }},
{"tools/call", [this](const json& req) { return ToolsCallCmd(req); }},
{"prompts/list", [this](const json& req) { return PromptsListCmd(req); }},
{"prompts/get", [this](const json& req) { return PromptsGetCmd(req); }},
{"resources/list", [this](const json& req) { return ResourcesListCmd(req); }},
{"resources/read", [this](const json& req) { return ResourcesReadCmd(req); }},
// ... 20+ 个方法注册
};
}
functionMap 类型是 std::unordered_map<std::string, std::function<json(const json&)>>------键是 MCP 方法名,值是处理函数(lambda / std::function)。
6.2 HandleRequest:请求分发
cpp
json Server::HandleRequest(const json& request) {
// 检查 method 字段
if (!request.contains("method")) {
return MCPBuilder::Error(MCPBuilder::InvalidRequest, request["id"], "Missing method");
}
// ★ 从 functionMap 中查找对应的处理函数
std::string methodName = request["method"];
auto it = functionMap.find(methodName);
if (it != functionMap.end()) {
json response = it->second(request); // 调用处理函数
return response;
}
// 方法未找到
return MCPBuilder::Error(MCPBuilder::MethodNotFound, std::to_string(request["id"]), "Method not found");
}
6.3 OverrideCallback:替换默认处理函数
cpp
bool Server::OverrideCallback(const std::string& method, std::function<json(const json&)> function) {
if (functionMap.find(method) != functionMap.end()) {
functionMap[method] = std::move(function); // ★ 用新函数覆盖旧函数
return true;
}
return false; // 方法名不存在则覆盖失败
}
为什么叫 Override 而不是 Register?
因为 Server 构造函数已经为所有 MCP 方法注册了默认 处理函数(返回空结果)。OverrideCallback 是用 main.cpp 中包含插件逻辑的 lambda 覆盖这些默认函数。这样 Server 本身不需要知道插件的存在,保持了单一职责。
七、完整请求流程:从客户端到插件再返回
以 tools/call 调用 calculator 的 add 工具为例:
步骤 1: 客户端发送 JSON-RPC 请求
────────────────────────────────────────
{"jsonrpc":"2.0", "method":"tools/call", "id":42,
"params":{"name":"add", "arguments":{"a":10, "b":20}}}
│ transport->Read()
▼
步骤 2: Server::HandleRequest()
────────────────────────────────────────
request["method"] = "tools/call"
functionMap.find("tools/call") → 找到 OverrideCallback 注册的闭包
│ 调用闭包
▼
步骤 3: OverrideCallback 闭包(main.cpp 中定义)
────────────────────────────────────────
遍历 loader->GetPlugins():
plugin[0] = calculator-tools (PLUGIN_TYPE_TOOLS)
GetTool(0) → name="calculator" ≠ "add"
GetTool(1) → name="add" = "add" ✅ 匹配!
│ plugin.instance->HandleRequest(request.dump().c_str())
▼
步骤 4: Calculator::HandleRequestImpl()(在 .dylib 中执行)
────────────────────────────────────────
json::parse(req) → toolName = "add", args = {"a":10, "b":20}
result = 10 + 20 = 30
response = {"content":[{"type":"text","text":"10+20=30"}], "isError":false}
return new char[](response.dump())
│ 返回 char*
▼
步骤 5: 闭包接收结果
────────────────────────────────────────
res_ptr = "{"content":[...],"isError":false}"
response["result"] = json::parse(res_ptr)
delete[] res_ptr ← 释放插件分配的内存
│ return response
▼
步骤 6: Server::Connect() 主循环
────────────────────────────────────────
transport_->Write(response.dump())
│ transport_->Write()
▼
步骤 7: 客户端收到响应
────────────────────────────────────────
{"jsonrpc":"2.0", "id":42,
"result":{"content":[{"type":"text","text":"10+20=30"}], "isError":false}}
八、已有插件一览
| 插件 | 文件 | 类型 | 工具 | 说明 |
|---|---|---|---|---|
| calculator | calculator/Calculator.cpp |
TOOLS | calculator, add, subtract, multiply, divide, power, sqrt, factorial | 数学计算,含递归下降表达式解析器 |
| weather | weather/Weather.cpp |
TOOLS | get_weather | 使用 cpp-httplib 调用 open-meteo.com API |
| notification | notification/Notification.cpp |
TOOLS | progress_test, logging_test | 演示插件向客户端推送通知/进度 |
| code-review | code-review/ |
TOOLS | code_review | 代码审查工具 |
| bacio-quote | bacio-quote/ |
TOOLS | get_quote | 名言警句 |
| sleep | sleep/ |
TOOLS | sleep_test | 测试长时间运行的任务 |
所有插件都遵循同一个模式:
- 定义
static PluginTool methods[]数组 - 实现 GetName/GetVersion/GetType/Initialize/HandleRequest/Shutdown
- 实现 GetToolCount/GetTool(或 GetPromptCount/GetPrompt 等)
- 组装
static PluginAPI plugin = { ... } - 导出
extern "C" CreatePlugin()/DestroyPlugin() - CMake:
add_library(xxx SHARED ...)
九、设计模式分析
9.1 插件模式(Plugin Pattern)
整个系统就是经典的插件架构:
- 主机(Host):MCP Server 主进程
- 接口契约(Contract):PluginAPI 结构体
- 插件(Plugin):各个 .so/.dylib 动态库
- 加载器(Loader):PluginsLoader
9.2 工厂方法(Factory Method)
CreatePlugin() / DestroyPlugin() 是工厂方法------主服务不知道插件内部的实现类,只通过工厂函数获取 PluginAPI*。
9.3 回调模式(Callback Pattern)
OverrideCallback 允许 main.cpp 用闭包替换 Server 的默认处理逻辑。闭包捕获 loader 引用,实现了Server 和 PluginsLoader 的解耦------Server 不直接依赖 PluginsLoader。
9.4 手写 vtable
PluginAPI 结构体中的函数指针本质上是一个手写的虚函数表,但使用 C ABI 保证了跨编译器兼容:
| C++ 虚函数 | PluginAPI 等价物 |
|---|---|
virtual const char* getName() = 0; |
const char* (*GetName)(); |
virtual void shutdown() = 0; |
void (*Shutdown)(); |
vptr → vtable[0] |
plugin.GetName(直接访问结构体成员) |
new ConcretePlugin() |
CreatePlugin() |
delete plugin; |
DestroyPlugin(plugin) |
十、面试话术
面试官:请介绍一下你的插件系统是怎么实现的?
我的 MCP Server 实现了一个基于动态库的插件系统。核心设计是定义了一个 C ABI 的
PluginAPI结构体,里面包含 12 个函数指针(GetName、GetType、HandleRequest、GetToolCount 等),以及CreatePlugin/DestroyPlugin两个工厂函数,用extern "C"导出。插件以
.so(Linux)/.dylib(macOS)/.dll(Windows)的形式独立编译。主服务通过PluginsLoader在启动时使用dlopen加载动态库、dlsym查找CreatePlugin符号、调用CreatePlugin()获取 PluginAPI 实例、调用Initialize()初始化。然后在 main.cpp 中通过
OverrideCallback机制,用捕获了loader的 lambda 替换 Server 的默认路由处理。比如tools/call的处理逻辑是:遍历所有 TOOLS 类型插件 → 遍历每个插件的工具列表 → 通过GetTool(i)->name匹配请求中的工具名 → 调用匹配插件的HandleRequest()。PluginAPI 支持三种类型:Tools(工具调用)、Prompts(提示模板)、Resources(数据资源),通过
GetType()区分,对应 MCP 协议中的tools/list、prompts/list、resources/list等不同方法。选择 C ABI 而非 C++ 虚函数,是因为插件和主服务可能用不同版本的编译器编译。C++ 的 vtable 布局和 name mangling 在不同编译器间不保证兼容,而 C 函数的调用约定在同一平台上是统一的。
PluginAPI本质上就是一个手写的 vtable。
面试官:新增一个插件需要改主服务代码吗?完全不需要。因为
PluginsLoader::LoadPlugins()使用std::filesystem::recursive_directory_iterator自动扫描plugins/目录下所有.so/.dylib文件。只要新插件实现了PluginAPI接口并正确导出CreatePlugin,把编译出来的 .so 文件放到plugins/目录下,重启主服务就会自动发现和加载------不需要修改或重新编译主服务。
面试官:HandleRequest 返回char*不怕内存泄漏吗?HandleRequest 通过
new char[]在堆上分配内存,调用方(main.cpp 中的 OverrideCallback 闭包)在解析完 JSON 后立即delete[] res_ptr释放。这个设计是故意的:跨动态库边界不能传std::string(不同编译器的 string 实现可能不同),只能传 C 字符串。内存的分配在插件侧(HandleRequest),释放在主服务侧(delete[]),只要双方都用new/delete(调同一个 CRT 的 allocator),就没问题。
面试官:tools/call 的工具查找是 O(n) 遍历,效率怎么样?当前实现确实是双层 O(n) 遍历(遍历插件 × 遍历工具)。对于十几个插件、每个几十个工具的规模,这完全可以接受。如果需要优化,可以在加载时建立
unordered_map<toolName, PluginEntry*>索引表,将查找降到 O(1)。但在实际场景中,工具数量很少是性能瓶颈------网络延迟和 LLM 调用才是主要开销。
三方工具插件实现:天气查询、代码审查、文件资源读取等
简历原文:(6)三方工具插件实现:编写天气查询、代码审查、文件资源读取等插件
本文档与 <plugin-system-study.md> 配合阅读。前者讲解插件框架本身 (PluginAPI 接口、PluginsLoader 加载器、Server 路由机制),本文聚焦于每个具体插件的实现细节,重点是天气查询(Weather)、代码审查(CodeReview)、文件资源读取(BacioQuote),以及通知(Notification)和休眠(Sleep)插件。
一、插件全景:三大类型 × 六个插件
┌──────────────────────────────────────────────────────┐
│ 插件目录结构 │
│ mcp_server_integrated/plugins/ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ TOOLS 类型 │ │ PROMPTS 类型 │ │RESOURCES类型│ │
│ │ │ │ │ │ │ │
│ │ calculator/ │ │ code-review/ │ │bacio-quote/│ │
│ │ weather/ │ │ │ │ │ │
│ │ notification/│ │ │ │ │ │
│ │ sleep/ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
│ 6 个插件 × 3 种类型 │
│ 每个插件独立编译为 .so/.dylib/.dll │
└──────────────────────────────────────────────────────┘
| 插件 | 类型 | 核心能力 | 外部依赖 | 文件 |
|---|---|---|---|---|
| weather | TOOLS | HTTP API 调用外部天气服务 | cpp-httplib | weather/Weather.cpp |
| code-review | PROMPTS | 生成 LLM 提示模板 | 无 | code-review/CodeReview.cpp |
| bacio-quote | RESOURCES | 随机读取数据资源 | MCPBuilder | bacio-quote/BacioQuote.cpp |
| notification | TOOLS | 演示插件→客户端通知推送 | MCPBuilder | notification/Notification.cpp |
| sleep | TOOLS | 模拟长时间任务 | 无 | sleep/Sleep.cpp |
| calculator | TOOLS | 数学运算(已在 plugin-system-study.md 详解) | 无 | calculator/Calculator.cpp |
所有插件路径的完整前缀为
mcp_server_integrated/plugins/
二、Weather 插件:天气查询(TOOLS 类型 + HTTP 外部调用)
这是最典型的**"有外部 IO 的 Tools 插件"**------通过 HTTP 调用第三方天气 API,解析返回数据,格式化为人类可读的天气预报。
2.1 引入的头文件
cpp
#include "PluginAPI.h" // 插件接口定义(C ABI)
#include "json.hpp" // nlohmann/json
#include "httplib.h" // cpp-httplib ------ 轻量级 HTTP 客户端/服务端库
using json = nlohmann::json;
cpp-httplib 是一个 header-only 的 C++ HTTP 库,既支持客户端请求也支持服务端。Weather 插件用它的 httplib::Client 发起 GET 请求。
2.2 工具定义:JSON Schema
cpp
static PluginTool methods[] = {
{
"get_weather", // ★ 工具名------客户端通过这个名字调用
"Get weather forecast of a city in the world. just pass as parameter "
"the latitude and longitude of the city you want to know the weather forecast.",
R"({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"latitude": { "type": "string" },
"longitude": { "type": "string" },
"city": { "type": "string" }
},
"required": ["city", "latitude", "longitude"],
"additionalProperties": false
})"
}
};
JSON Schema 的作用:
- 给 LLM 看 :LLM(如通义千问)看到
inputSchema后,知道应该把参数组织成{"latitude":"39.9", "longitude":"116.4", "city":"Beijing"}的格式 - 给客户端校验:客户端可以在发送前用 JSON Schema 验证参数合法性
- 给
tools/list返回 :主服务在响应tools/list时把这个 Schema 原样返回给客户端
2.3 HandleRequest:核心处理流程
cpp
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
// ═══════════════════════════════════════════════
// 第 ① 步:从 JSON-RPC 请求中提取参数
// ═══════════════════════════════════════════════
auto latitude = request["params"]["arguments"]["latitude"].get<std::string>();
auto longitude = request["params"]["arguments"]["longitude"].get<std::string>();
auto city = request["params"]["arguments"]["city"].get<std::string>();
nlohmann::json weatherContent;
// ═══════════════════════════════════════════════
// 第 ② 步:构建 HTTP 客户端,调用 Open-Meteo API
// ═══════════════════════════════════════════════
httplib::Client cli("api.open-meteo.com");
auto res = cli.Get(
"/v1/forecast?latitude=" + latitude +
"&longitude=" + longitude +
"&hourly=temperature_2m&forecast_days=1"
);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Open-Meteo:免费、无需 API Key 的天气 API
// 参数:经纬度 + hourly=temperature_2m(逐小时温度)+ 预报 1 天
// 返回 JSON 格式的逐小时温度数据
Open-Meteo API 返回示例:
json
{
"hourly": {
"time": ["2025-01-01T00:00", "2025-01-01T01:00", ... (24个)],
"temperature_2m": [5.2, 4.8, 4.5, ... (24个)]
}
}
cpp
// ═══════════════════════════════════════════════
// 第 ③ 步:解析 API 响应,格式化为人类可读的天气预报
// ═══════════════════════════════════════════════
if (res && res->status == 200) {
auto weatherData = json::parse(res->body);
std::stringstream weatherMessage;
weatherMessage << "Weather Forecast for " << city << ":\n\n";
auto times = weatherData["hourly"]["time"];
auto temperatures = weatherData["hourly"]["temperature_2m"];
// ── 分时段计算平均温度 ──
// 🌅 Morning (6:00-12:00)
double morningTemp = 0.0;
for (int i = 6; i < 12; i++) {
morningTemp += temperatures[i].get<double>();
}
morningTemp /= 6;
weatherMessage << "🌅 Morning: " << std::fixed << std::setprecision(1)
<< morningTemp << "°C\n";
// ☀️ Afternoon (12:00-18:00)
double afternoonTemp = 0.0;
for (int i = 12; i < 18; i++) {
afternoonTemp += temperatures[i].get<double>();
}
afternoonTemp /= 6;
weatherMessage << "☀️ Afternoon: " << std::fixed << std::setprecision(1)
<< afternoonTemp << "°C\n";
// 🌙 Evening (18:00-24:00)
double eveningTemp = 0.0;
for (int i = 18; i < 24; i++) {
eveningTemp += temperatures[i].get<double>();
}
eveningTemp /= 6;
weatherMessage << "🌙 Evening: " << std::fixed << std::setprecision(1)
<< eveningTemp << "°C\n\n";
// ── 全天最高/最低温度 ──
double maxTemp = temperatures[0].get<double>();
double minTemp = temperatures[0].get<double>();
std::string maxTime, minTime;
for (size_t i = 0; i < temperatures.size(); i++) {
double temp = temperatures[i].get<double>();
if (temp > maxTemp) {
maxTemp = temp;
maxTime = times[i].get<std::string>().substr(11, 5); // "2025-01-01T14:00" → "14:00"
}
if (temp < minTemp) {
minTemp = temp;
minTime = times[i].get<std::string>().substr(11, 5);
}
}
weatherMessage << "🔼 Highest: " << maxTemp << "°C at " << maxTime << "\n";
weatherMessage << "🔽 Lowest: " << minTemp << "°C at " << minTime << "\n\n";
// ── 智能总结 ──
weatherMessage << "📊 Daily Summary: ";
if (maxTemp > 25) weatherMessage << "Hot day! ";
else if (maxTemp > 20) weatherMessage << "Warm day. ";
else if (maxTemp > 10) weatherMessage << "Mild temperatures. ";
else weatherMessage << "Cool day. ";
double tempVariation = maxTemp - minTemp;
if (tempVariation > 10)
weatherMessage << "Large temperature variation throughout the day.";
else if (tempVariation > 5)
weatherMessage << "Moderate temperature changes expected.";
else
weatherMessage << "Fairly consistent temperatures today.";
weatherContent["type"] = "text";
weatherContent["text"] = weatherMessage.str();
} else {
// ★ HTTP 请求失败时的降级处理
weatherContent["type"] = "text";
weatherContent["text"] = "Cannot get weather forecast for " + city + ".";
}
cpp
// ═══════════════════════════════════════════════
// 第 ④ 步:构建 MCP 标准响应格式
// ═══════════════════════════════════════════════
nlohmann::json response;
response["content"] = json::array();
response["content"].push_back(weatherContent);
response["isError"] = false;
// ★ 返回堆分配的 C 字符串
std::string result = response.dump();
char* buffer = new char[result.length() + 1];
strcpy(buffer, result.c_str());
return buffer;
}
2.4 PluginAPI 组装 & CMake
cpp
// 身份信息
const char* GetNameImpl() { return "weather-tools"; }
const char* GetVersionImpl() { return "1.0.0"; }
PluginType GetTypeImpl() { return PLUGIN_TYPE_TOOLS; } // ★ Tools 类型
// 组装函数指针表
static PluginAPI plugin = {
GetNameImpl, GetVersionImpl, GetTypeImpl,
InitializeImpl, HandleRequestImpl, ShutdownImpl,
GetToolCountImpl, GetToolImpl, // Tools 元数据
nullptr, nullptr, // Prompts(不用)
nullptr, nullptr // Resources(不用)
};
extern "C" PLUGIN_API PluginAPI* CreatePlugin() { return &plugin; }
extern "C" PLUGIN_API void DestroyPlugin(PluginAPI*) {}
cmake
add_library(weather SHARED
${PROJECT_SOURCE_DIR}/plugins/weather/Weather.cpp
)
if(UNIX)
set_target_properties(weather PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()
# Windows 需要链接 ws2_32(Winsock 套接字库),因为 httplib 用了网络功能
if(WIN32)
target_link_libraries(weather PRIVATE ws2_32)
endif()
target_include_directories(weather PRIVATE
${PROJECT_SOURCE_DIR}/include # json.hpp, httplib.h
${PROJECT_SOURCE_DIR}/src/interface # PluginAPI.h
)
与 Calculator 插件的区别 :Weather 额外引入了 httplib.h,并且在 Windows 上需要链接 ws2_32(Winsock 库)。在 macOS/Linux 上不需要额外的网络库链接。
2.5 Weather 的技术亮点
| 方面 | 实现 |
|---|---|
| 外部 API 调用 | 使用 cpp-httplib 发起同步 HTTP GET 请求 |
| 无需 API Key | 使用 Open-Meteo 免费天气 API |
| 数据处理 | 24 小时逐小时数据 → 分三段(早/午/晚)计算平均温度 |
| 智能摘要 | 根据最高温度和温差幅度生成自然语言总结 |
| 错误处理 | HTTP 失败时返回友好的错误消息 |
三、CodeReview 插件:代码审查(PROMPTS 类型)
这是项目中唯一的 Prompts 类型插件 ,与 Tools 类型有本质区别:它不执行具体计算,而是生成 LLM 提示消息,让 LLM 来完成代码审查任务。
3.1 Prompts vs Tools 的根本区别
┌──────────────────────────────────────────────────────┐
│ Tools 类型工作流 │
│ │
│ Client → tools/call → 插件 HandleRequest → 直接计算 │
│ 返回结果(如 2+3=5)给客户端 │
└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ Prompts 类型工作流 │
│ │
│ Client → prompts/get → 插件 HandleRequest │
│ → 返回 LLM 消息模板(role + content)给客户端 │
│ → 客户端把这些消息发给 LLM │
│ → LLM 执行代码审查并返回结果 │
└──────────────────────────────────────────────────────┘
3.2 Prompt 元数据定义
cpp
static PluginPrompt prompts[] = {
{
"code-review", // ★ Prompt 名称
"Asks the LLM to analyze code quality and suggest improvements",
// ★ arguments 定义:描述该 Prompt 需要哪些参数
R"([{
"name" : "language",
"description" : "The programming language of the code",
"required": true
}])"
}
};
注意与 PluginTool 的区别:
PluginTool的第三个字段是inputSchema(JSON Schema 格式)PluginPrompt的第三个字段是arguments(参数列表 JSON 数组)
3.3 GetType 与元数据函数
cpp
const char* GetNameImpl() { return "code-review"; }
const char* GetVersionImpl() { return "1.0.0"; }
// ★ 声明为 PROMPTS 类型------决定了主服务把它注册到 prompts/list & prompts/get
PluginType GetTypeImpl() { return PLUGIN_TYPE_PROMPTS; }
// ★ 注意这里是 GetPromptCount / GetPrompt,不是 GetToolCount / GetTool
int GetPromptCountImpl() {
return sizeof(prompts) / sizeof(prompts[0]); // = 1
}
const PluginPrompt* GetPromptImpl(int index) {
if (index < 0 || index >= GetPromptCountImpl()) return nullptr;
return &prompts[index];
}
3.4 HandleRequest:生成 LLM 消息模板
cpp
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
// ★ 从请求中获取编程语言参数
auto language = request["params"]["arguments"]["language"].get<std::string>();
// ═══════════════════════════════════════════════
// 构建 MCP Prompt 响应格式
// ═══════════════════════════════════════════════
nlohmann::json response = json::object();
// ★ 构建 messages 数组------这是给 LLM 的消息序列
nlohmann::json messages = json::array();
messages.push_back(json::object({
{"role", "user"}, // 角色:用户
{"content", json::object({
{"type", "text"}, // 内容类型:文本
{"text", "Please analyze code quality and suggest improvements "
"of this code written in " + language} // ★ 动态拼接语言名
})}
}));
response["description"] = "this is the code review prompt";
response["messages"] = messages;
// 返回堆分配的 C 字符串
std::string result = response.dump();
char* buffer = new char[result.length() + 1];
strcpy(buffer, result.c_str());
return buffer;
}
返回的 JSON 结构:
json
{
"description": "this is the code review prompt",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please analyze code quality and suggest improvements of this code written in C++"
}
}
]
}
客户端收到这个消息模板后,可以:
- 在
messages数组中追加用户的代码内容 - 把整个
messages发给 LLM(如通义千问) - LLM 基于提示执行代码审查
3.5 PluginAPI 组装------与 Tools 类型的对比
cpp
static PluginAPI plugin = {
GetNameImpl, GetVersionImpl, GetTypeImpl,
InitializeImpl, HandleRequestImpl, ShutdownImpl,
nullptr, nullptr, // ★ GetToolCount/GetTool = nullptr(不是 Tools)
GetPromptCountImpl, GetPromptImpl, // ★ GetPromptCount/GetPrompt 有值
nullptr, nullptr // GetResourceCount/GetResource = nullptr
};
对比 calculator 插件:
cpp
// Calculator(TOOLS 类型)
static PluginAPI plugin = {
...,
GetToolCountImpl, GetToolImpl, // ★ Tools 元数据有值
nullptr, nullptr, // Prompts = nullptr
nullptr, nullptr // Resources = nullptr
};
关键区分 :PluginAPI 的 12 个函数指针中,每种类型只填充自己类型对应的 Get 函数,其余设为 nullptr。主服务通过 GetType() 判断类型后,只调用对应的 Get 函数。
3.6 主服务如何处理 Prompts 类型
回顾 main.cpp 中 prompts/get 的 OverrideCallback(plugin-system-study.md 第五章有完整代码):
cpp
server->OverrideCallback("prompts/get", [&loader](const json& request) {
for (const auto& plugin : loader->GetPlugins()) {
if (plugin.instance->GetType() == PLUGIN_TYPE_PROMPTS) { // 只看 Prompts 类型
for (int i = 0; i < plugin.instance->GetPromptCount(); i++) {
auto pluginPrompt = plugin.instance->GetPrompt(i);
if (pluginPrompt->name == request["params"]["name"]) { // 按 name 匹配
char* res_ptr = plugin.instance->HandleRequest(request.dump().c_str());
// ... 解析结果 → 返回给客户端
}
}
}
}
});
请求示例:
json
{"jsonrpc":"2.0", "method":"prompts/get", "id":5,
"params":{"name":"code-review", "arguments":{"language":"C++"}}}
完整响应:
json
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"description": "this is the code review prompt",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please analyze code quality and suggest improvements of this code written in C++"
}
}
]
}
}
四、BacioQuote 插件:文件资源读取(RESOURCES 类型)
这是项目中唯一的 Resources 类型插件 。Resources 类型的定位是提供可读取的数据源(文件、数据库、配置等),而不是执行计算或生成提示。
4.1 数据存储
BacioQuote 插件内嵌了一个包含 ~150 条意大利语名言的字符串数组:
cpp
std::vector<std::string> messages = {
"Amor che nella mente mi ragiona... (Dante)",
"Che cosa sarebbe l'umanità, signore, senza la donna? ... (Mark Twain)",
"A chi più amiamo, meno dire sappiamo. (Proverbio inglese)",
"Ama e fai quel che vuoi. (S. Agostino)",
// ... 约 150 条经典意大利语情话/名言
"La fortuna non è sempre e tutta opera del caso. (Baltasar Gracian)"
};
4.2 Resource 元数据定义
cpp
static PluginResource resources[] = {
{
"bacio-quote", // ★ 资源名称
"A list of the famous italian bacio perugina quotes", // 描述
"bacio:///quote", // ★ 资源 URI(自定义协议)
"text/plain", // MIME 类型
}
};
PluginResource 与 PluginTool 的关键区别:
PluginTool有inputSchema(参数 Schema)→ 用于执行PluginResource有uri+mime→ 用于读取- 资源通过 URI 标识,而非 name(尽管两者都有 name)
- 主服务在
resources/read中按uri匹配,而非按name匹配
4.3 GetType 与元数据
cpp
const char* GetNameImpl() { return "bacio-quote"; }
const char* GetVersionImpl() { return "1.0.0"; }
// ★ 声明为 RESOURCES 类型
PluginType GetTypeImpl() { return PLUGIN_TYPE_RESOURCES; }
int GetResourceCountImpl() {
return sizeof(resources) / sizeof(resources[0]); // = 1
}
const PluginResource* GetResourceImpl(int index) {
if (index < 0 || index >= GetResourceCountImpl()) return nullptr;
return &resources[index];
}
4.4 HandleRequest:随机读取资源
cpp
#include "../../src/utils/MCPBuilder.h" // 引入 MCPBuilder 工具类
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
nlohmann::json response = json::object();
// ★ 使用 C++11 随机数引擎,生成随机索引
std::random_device rd; // 硬件随机数种子
std::mt19937 gen(rd()); // Mersenne Twister 引擎
std::uniform_int_distribution<> distr(0, messages.size() - 1); // 均匀分布
// ★ 构建 MCP Resources 响应格式
nlohmann::json contents = json::array();
contents.push_back(
MCPBuilder::ResourceText( // MCPBuilder 辅助函数
resources[0].uri, // "bacio:///quote"
resources[0].mime, // "text/plain"
messages[distr(gen)] // 随机选一条名言
)
);
response["contents"] = contents;
// 返回堆分配的 C 字符串
std::string result = response.dump();
char* buffer = new char[result.length() + 1];
strcpy(buffer, result.c_str());
return buffer;
}
4.5 MCPBuilder::ResourceText 辅助函数
cpp
static json ResourceText(const std::string& uri, const std::string& mime, const std::string& text) {
return json::object({
{"uri", uri}, // "bacio:///quote"
{"mimeType", mime}, // "text/plain"
{"text", text} // "Ama e fai quel che vuoi. (S. Agostino)"
});
}
返回给客户端的 JSON 响应 (注意字段名是 contents 而非 content------Resources 和 Tools 的响应格式不同):
json
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"contents": [
{
"uri": "bacio:///quote",
"mimeType": "text/plain",
"text": "Ama e fai quel che vuoi. (S. Agostino)"
}
]
}
}
4.6 PluginAPI 组装
cpp
static PluginAPI plugin = {
GetNameImpl, GetVersionImpl, GetTypeImpl,
InitializeImpl, HandleRequestImpl, ShutdownImpl,
nullptr, nullptr, // Tools = nullptr
nullptr, nullptr, // Prompts = nullptr
GetResourceCountImpl, GetResourceImpl // ★ Resources 元数据有值
};
4.7 主服务如何处理 Resources 类型
main.cpp 中 resources/read 的 OverrideCallback:
cpp
server->OverrideCallback("resources/read", [&loader](const json& request) {
for (const auto& plugin : loader->GetPlugins()) {
if (plugin.instance->GetType() == PLUGIN_TYPE_RESOURCES) {
for (int i = 0; i < plugin.instance->GetResourceCount(); i++) {
auto pluginResource = plugin.instance->GetResource(i);
// ★ Resources 按 URI 匹配,不是按 name!
if (pluginResource->uri == request["params"]["uri"]) {
char* res_ptr = plugin.instance->HandleRequest(request.dump().c_str());
// ... 解析 & 返回
}
}
}
}
});
请求示例:
json
{"jsonrpc":"2.0", "method":"resources/read", "id":7,
"params":{"uri":"bacio:///quote"}}
五、Notification 插件:通知推送机制(TOOLS 类型 + 通知系统)
文件:mcp_server_integrated/plugins/notification/Notification.cpp
这个插件的特殊之处在于它演示了插件向客户端主动推送消息 的能力------通过主服务在启动时注入的 NotificationSystem 回调。
5.1 工具定义:两个测试工具
cpp
static PluginTool methods[] = {
{
"progress_test",
"Execute a long running process and inform the client about the progress",
R"({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
})"
},
{
"logging_test",
"Execute a logging test. Send a message from server to the client",
R"({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
})"
}
};
5.2 全局指针:保存插件自身引用
cpp
static PluginAPI* g_plugin = nullptr; // ★ 用于在 HandleRequest 中访问 notifications
extern "C" PLUGIN_API PluginAPI* CreatePlugin() {
g_plugin = &plugin; // ★ 在创建时保存全局引用
return &plugin;
}
为什么需要 g_plugin?因为 HandleRequestImpl 是一个普通 C 函数(extern "C"),不能访问 this。要使用 notifications 回调,需要通过全局指针 g_plugin->notifications->SendToClient(...) 调用。
5.3 logging_test 工具:发送日志通知
cpp
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
if (request["params"]["name"] == "logging_test") {
if (g_plugin) {
// ★ MCPBuilder::NotificationLog 构建日志通知 JSON
std::string message = MCPBuilder::NotificationLog(
"notice", // 日志级别
"****** THIS IS A LOGGING TEST!" // 日志内容
).dump();
// ★ 通过注入的回调函数,推送通知到客户端
g_plugin->notifications->SendToClient(
GetNameImpl(), // "notification-tools"(插件名)
message.c_str() // JSON 格式的通知消息
);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
MCPBuilder::NotificationLog 生成的 JSON 格式:
json
{
"jsonrpc": "2.0",
"method": "notifications/message",
"params": {
"level": "notice",
"data": "****** THIS IS A LOGGING TEST!"
}
}
这条消息通过 Server 的通知队列推送给客户端------是一条单向通知 (没有 id 字段,不需要响应)。
5.4 progress_test 工具:进度推送
cpp
else if (request["params"]["name"] == "progress_test") {
const int totalDuration = 10; // 总计 10 秒
// ★ 检查请求中是否包含 progressToken
if (!request["params"].contains("_meta") ||
!request["params"]["_meta"].contains("progressToken")) {
// 没有 progressToken → 返回错误
nlohmann::json errorResponse;
errorResponse["content"] = json::array();
errorResponse["content"].push_back(
MCPBuilder::TextContent("Missing required parameter: progressToken."));
errorResponse["isError"] = true;
// ... 返回错误响应
}
std::string progressToken = request["params"]["_meta"]["progressToken"].get<std::string>();
if (g_plugin) {
// ★ 每秒推送一次进度通知,共 10 次
for (int i = 1; i <= totalDuration; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
int progressPercent = (i * 100) / totalDuration; // 10%, 20%, ..., 100%
std::string progressMessage = "Progress: " + std::to_string(progressPercent) + "%";
// ★ 构建进度通知
std::string message = MCPBuilder::NotificationProgress(
progressMessage, // "Progress: 30%"
progressToken, // 客户端提供的 token,用于关联请求
progressPercent, // 当前进度:30
100 // 总量:100
).dump();
// ★ 推送到客户端
g_plugin->notifications->SendToClient(GetNameImpl(), message.c_str());
}
}
}
MCPBuilder::NotificationProgress 生成的 JSON 格式:
json
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "token-abc-123",
"progress": 30,
"total": 100,
"message": "Progress: 30%"
}
}
5.5 通知推送的完整数据流
插件 HandleRequest 执行中
│
│ g_plugin->notifications->SendToClient("notification-tools", json)
▼
main.cpp: ClientNotificationCallbackImpl()
│ ← 主服务在启动时注入的回调函数
│
│ server->SendNotification(json)
▼
Server.cpp: SendNotification()
│
│ {
│ std::lock_guard<std::mutex> lock(queue_mutex_);
│ notification_queue_.push(message); // 推入通知队列
│ queue_cv_.notify_one(); // 唤醒 WriterLoop
│ }
▼
Server.cpp: WriterLoop (后台线程)
│
│ transport_->Write(notification) // 通过 transport 写给客户端
▼
客户端收到通知
六、Sleep 插件:模拟长时间任务(TOOLS 类型)
最简单的插件,完整代码不到 100 行,用于测试主服务处理长时间运行请求的能力。
6.1 工具定义
cpp
static PluginTool methods[] = {
{
"sleep",
"Pauses execution for the specified number of milliseconds.",
R"({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"milliseconds": {
"type": "number",
"minimum": 0,
"description": "Number of milliseconds to sleep."
}
},
"required": ["milliseconds"],
"additionalProperties": false
})"
}
};
注意 JSON Schema 中的 "minimum": 0 约束------不允许传负数。
6.2 HandleRequest
cpp
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
// 获取毫秒数参数
auto milliseconds = request["params"]["arguments"]["milliseconds"].get<int>();
// ★ 核心:阻塞当前线程指定的毫秒数
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
// 构建响应
nlohmann::json responseContent;
responseContent["type"] = "text";
responseContent["text"] = "Waited for " + std::to_string(milliseconds) + " milliseconds";
nlohmann::json response;
response["content"] = json::array();
response["content"].push_back(responseContent);
response["isError"] = false;
std::string result = response.dump();
char* buffer = new char[result.length() + 1];
strcpy(buffer, result.c_str());
return buffer;
}
七、三类插件的响应格式对比
7.1 list 响应格式
| 方法 | 响应字段 | 内容 |
|---|---|---|
tools/list |
result.tools[] |
{name, description, inputSchema} |
prompts/list |
result.prompts[] |
{name, description, arguments} |
resources/list |
result.resources[] |
{name, description, uri, mimeType} |
7.2 call/get/read 响应格式
| 方法 | 匹配字段 | 响应结构 |
|---|---|---|
tools/call |
params.name |
result.content[] + result.isError |
prompts/get |
params.name |
result.messages[] + result.description |
resources/read |
params.uri ★ 注意是 URI |
result.contents[](有 uri + mimeType + text) |
7.3 PluginAPI 函数指针填充对比
Tools Prompts Resources
GetToolCount ✅ 有值 nullptr nullptr
GetTool ✅ 有值 nullptr nullptr
GetPromptCount nullptr ✅ 有值 nullptr
GetPrompt nullptr ✅ 有值 nullptr
GetResourceCount nullptr nullptr ✅ 有值
GetResource nullptr nullptr ✅ 有值
八、插件开发模板:快速新增一个插件的步骤
如果要新增一个 Tools 类型插件(例如 translator 翻译工具),只需 3 步:
8.1 创建插件源码
mcp_server_integrated/plugins/translator/Translator.cpp
cpp
#include "PluginAPI.h"
#include "json.hpp"
using json = nlohmann::json;
// ① 定义工具
static PluginTool methods[] = {
{"translate", "Translates text between languages",
R"({"type":"object","properties":{
"text":{"type":"string"}, "from":{"type":"string"}, "to":{"type":"string"}
},"required":["text","from","to"]})"}
};
// ② 实现身份 + 处理函数
const char* GetNameImpl() { return "translator-tools"; }
const char* GetVersionImpl() { return "1.0.0"; }
PluginType GetTypeImpl() { return PLUGIN_TYPE_TOOLS; }
int InitializeImpl() { return 1; }
void ShutdownImpl() {}
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
auto text = request["params"]["arguments"]["text"].get<std::string>();
auto from = request["params"]["arguments"]["from"].get<std::string>();
auto to = request["params"]["arguments"]["to"].get<std::string>();
// ... 调用翻译 API ...
std::string translated = "翻译结果"; // 实际应调用外部 API
json response;
response["content"] = json::array();
response["content"].push_back({{"type","text"},{"text", translated}});
response["isError"] = false;
std::string result = response.dump();
char* buffer = new char[result.length() + 1];
strcpy(buffer, result.c_str());
return buffer;
}
int GetToolCountImpl() { return sizeof(methods) / sizeof(methods[0]); }
const PluginTool* GetToolImpl(int index) {
if (index < 0 || index >= GetToolCountImpl()) return nullptr;
return &methods[index];
}
// ③ 组装 PluginAPI + 导出
static PluginAPI plugin = {
GetNameImpl, GetVersionImpl, GetTypeImpl,
InitializeImpl, HandleRequestImpl, ShutdownImpl,
GetToolCountImpl, GetToolImpl,
nullptr, nullptr, nullptr, nullptr
};
extern "C" PLUGIN_API PluginAPI* CreatePlugin() { return &plugin; }
extern "C" PLUGIN_API void DestroyPlugin(PluginAPI*) {}
8.2 创建 CMakeLists.txt
mcp_server_integrated/plugins/translator/CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.10)
add_library(translator SHARED
${PROJECT_SOURCE_DIR}/plugins/translator/Translator.cpp
)
if(UNIX)
set_target_properties(translator PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()
find_package(Threads REQUIRED)
target_link_libraries(translator PRIVATE Threads::Threads)
target_include_directories(translator PRIVATE
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/src/interface
)
8.3 编译 → 放入 plugins/ 目录 → 重启主服务
bash
# 编译后,动态库自动生成在 build 目录中
# macOS: libtranslator.dylib
# Linux: libtranslator.so
# 放入 plugins/ 目录即可被自动发现
# ★ 不需要修改主服务的任何代码!
九、设计分析与面试话术
9.1 每个插件如何隔离依赖
| 插件 | 特殊依赖 | CMake 链接 |
|---|---|---|
| calculator | 无 | Threads::Threads |
| weather | httplib.h | Threads::Threads + ws2_32(Windows) |
| code-review | 无 | Threads::Threads |
| bacio-quote | MCPBuilder.h | Threads::Threads |
| notification | MCPBuilder.h | Threads::Threads |
| sleep | 无 | Threads::Threads |
每个插件只 #include 两个核心头文件(PluginAPI.h + json.hpp),加上各自需要的库(如 httplib.h)。不依赖主服务的任何编译产物(.o / .a),真正做到了编译隔离。
9.2 三种类型的适用场景
┌───────────────────────────────────────────────────────────────┐
│ │
│ TOOLS:插件自己执行 → 返回结果 │
│ 典型:计算器、天气查询、数据库操作 │
│ 请求方法:tools/call │
│ 匹配方式:params.name │
│ │
│ PROMPTS:插件生成 LLM 提示模板 → 客户端发给 LLM 执行 │
│ 典型:代码审查、文本摘要、翻译提示 │
│ 请求方法:prompts/get │
│ 匹配方式:params.name │
│ │
│ RESOURCES:插件提供可读取的数据内容 │
│ 典型:文件读取、配置查询、知识库检索 │
│ 请求方法:resources/read │
│ 匹配方式:params.uri(注意!不是 name) │
│ │
└───────────────────────────────────────────────────────────────┘
9.3 面试话术
面试官:你说你实现了天气查询、代码审查和文件资源读取等插件,具体怎么实现的?
这三个插件分别对应 MCP 协议的三种能力类型。
天气查询 (Weather)是 Tools 类型插件,它在插件内部通过 cpp-httplib 的
httplib::Client向 Open-Meteo 天气 API 发起 HTTP GET 请求,把经纬度作为参数传递,获取 24 小时逐小时温度数据,然后在插件内部将原始数据处理成按早/午/晚三段的平均温度、全天最高最低温度,以及智能摘要文字。这是一个完整的"接收参数 → 外部 IO → 数据处理 → 返回结果"的 Tools 类型流程。代码审查 (CodeReview)是 Prompts 类型插件------它自己不做审查,而是根据用户选择的编程语言,动态生成一段 LLM 提示消息(包含
role: user和具体的审查指令文本)。客户端拿到这个消息模板后,追加用户要审查的代码,再发给 LLM 执行。所以 Prompts 类型的的本质是提示工程的模板化。文件资源读取 (BacioQuote)是 Resources 类型插件,它通过 URI(如
bacio:///quote)标识一个数据资源。当客户端发resources/read请求时,插件从内嵌的名言数据中随机选取一条,通过MCPBuilder::ResourceText包装成带 URI、MIME 类型和文本内容的标准格式返回。Resources 的匹配不是按工具名,而是按 URI。三种类型的
PluginAPI结构体中,每个插件只填自己类型对应的GetXxxCount/GetXxx函数指针,其余设为nullptr。主服务通过GetType()判断类型后路由到tools/call、prompts/get或resources/read对应的处理逻辑。
面试官:Weather 插件的 HTTP 请求是同步的,这不会阻塞主服务吗?确实,当前 Weather 插件中
httplib::Client::Get()是同步阻塞的。但这不会阻塞整个主服务,因为请求处理发生在主服务为该客户端连接分配的线程中(STDIO transport 是单连接的主线程,SSE transport 的每个请求由 httplib 的线程池处理)。其他客户端的请求不受影响。如果需要优化,可以:
- 在插件内部使用
std::async或线程池异步发起 HTTP 请求- 对 httplib::Client 设置超时(
cli.set_read_timeout(5, 0))- 添加缓存层,对同一城市的短时间内重复查询直接返回缓存结果
面试官:Notification 插件的进度推送是怎么实现的?和普通的 tools/call 响应有什么区别?普通的
tools/call是"请求-响应"模式:客户端发一个请求,等一个响应。而 Notification 插件演示了"请求 + 多次通知 + 最终响应"的模式。插件在 HandleRequest 执行过程中,通过主服务在启动时注入的
NotificationSystem回调(g_plugin->notifications->SendToClient()),在返回最终结果之前 就把进度信息推送给客户端。推送的消息是 JSON-RPC 通知格式(没有id字段,不需要客户端回应),通过 Server 的通知队列和 WriterLoop 线程写出。具体实现是在
CreatePlugin()中保存全局指针g_plugin = &plugin,这样 HandleRequest 函数(C ABI 函数,没有 this)就能通过全局指针访问通知系统。
面试官:Prompts 类型和 Tools 类型可以合并吗?为什么要分开?从技术上完全可以用一个 Tools 插件同时返回提示文本,但 MCP 协议特意做了这个区分,因为它们的语义不同:
- Tools 是"此处需要执行"------插件内部完成计算/IO,直接返回结果
- Prompts 是"此处需要 LLM 参与"------插件只提供提示模板,真正的执行由 LLM 完成
这个区分让客户端(或 AI Agent)知道:调用
tools/call后可以直接用结果,而调用prompts/get后需要把结果发给 LLM 继续处理。这是一种意图声明,帮助 Agent 编排管线做出正确的决策。
基于语义检索的智能工具选择框架(RAG-MCP)
简历原文:基于语义检索的智能工具选择框架开发:(1)对于开发的三方tools工具,将每个工具的名称、描述、参数信息组合成文本,调用 Embedding API转换为向量,存储在向量索引中;(2)将用户查询转换为向量,在向量索引中搜索最相似的工具向量,只保留前k个,返回对应的工具,发送给大语言模型。由LLM从中选择最适合的工具来执行用户的请求;避免在当 MCP Server 提供大量工具时,将所有工具发送给LLM,导致token消耗过大、选择准确率下降等多问题
一、为什么需要 RAG-MCP?解决什么问题?
当 MCP Server 上注册了大量工具(几十甚至上百个)时,如果把所有工具的定义(名称 + 描述 + 参数 Schema)全部塞进 LLM 的 prompt,会面临以下问题:
| 问题 | 说明 |
|---|---|
| Token 爆炸 | 每个工具的 JSON Schema 可能有几百 token,100 个工具 = 几万 token |
| 选择准确率下降 | 工具太多时 LLM 容易"看花眼",选错工具 |
| 延迟增加 | prompt 越长,LLM 推理时间越长 |
| 成本增加 | Token 数量直接决定 API 调用费用 |
解决方案:RAG(Retrieval-Augmented Generation)
传统方式: RAG-MCP 方式:
用户查询 + 全部工具(100个) 用户查询 ──→ Embedding ──→ 向量搜索
↓ ↓
发给 LLM(几万 token) 用户查询 + 最相关的工具(5个)
↓ ↓
LLM 从 100 个中选 发给 LLM(几千 token)
↓
LLM 从 5 个中选(更精准)
二、整体架构:五个核心组件
┌─────────────────────────────────────────────────────────────┐
│ MCPAgentIntegration │
│ (统一集成入口) │
│ │
│ getRelevantTools(query) ──→ ToolRetriever.retrieve() │
│ │ │ │
│ │ ┌────────┴────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │EmbeddingCache│ │EmbeddingServ.│ │ VectorIndex │ │
│ │ (LRU 缓存) │ │ (DashScope) │ │ (内存索引) │ │
│ │ │ │ │ │ │ │
│ │ get/put │ │ embed() │ │ search() │ │
│ │ 命中→直接返回 │ │ embedBatch() │ │ addTool() │ │
│ │ 未命中→调API │ │ curl POST │ │ saveTo/Load │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 可选: ToolValidator (验证检索到的工具是否可用) │
└─────────────────────────────────────────────────────────────┘
| 组件 | 文件 | 职责 |
|---|---|---|
| EmbeddingService | mcp/src/rag/embedding_service.cpp | 调用 DashScope API 将文本转为向量 |
| EmbeddingCache | mcp/src/rag/embedding_cache.cpp | LRU 缓存,避免重复 API 调用 |
| VectorIndex | mcp/src/rag/vector_index.cpp | 内存向量索引,余弦相似度搜索 |
| ToolRetriever | mcp/src/rag/tool_retriever.cpp | 整合上述三者,提供完整的检索流程 |
| ToolValidator | mcp/src/rag/tool_validator.cpp | 可选的工具可用性验证 |
| MCPAgentIntegration | mcp/src/mcp_agent_integration.cpp | 统一集成入口,对外暴露 getRelevantTools() |
三、EmbeddingService:调用 DashScope API 生成向量
3.1 配置
cpp
struct EmbeddingConfig {
std::string api_key; // DashScope API Key
std::string model = "text-embedding-v2"; // 阿里 text-embedding-v2 模型
int dimension = 1536; // 向量维度(1536 维)
int max_retries = 3; // 最大重试次数
int timeout_ms = 10000; // HTTP 超时 10 秒
int initial_retry_delay_ms = 1000; // 初始重试延迟 1 秒
std::string api_url = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding";
bool loadApiKeyFromEnv(); // 从环境变量 DASHSCOPE_API_KEY 读取
bool validate() const; // 检查配置是否合法
};
3.2 embed() 和 embedBatch()
cpp
// 单文本向量化
std::vector<float> EmbeddingService::embed(const std::string& text) {
auto results = embedBatch({text}); // 内部复用批量接口
if (results.empty()) {
throw std::runtime_error("Empty embedding result");
}
return results[0];
}
// 批量向量化
std::vector<std::vector<float>> EmbeddingService::embedBatch(
const std::vector<std::string>& texts) {
if (texts.empty()) return {};
if (!config_.validate()) {
throw std::runtime_error("Invalid EmbeddingConfig: API key may be missing");
}
// ★ 构造 DashScope API 请求体
json request_body = {
{"model", config_.model}, // "text-embedding-v2"
{"input", {
{"texts", texts_array} // ["text1", "text2", ...]
}},
{"parameters", {
{"text_type", "query"} // 查询类型
}}
};
// 带重试的 API 调用
std::string response = callApiWithRetry(request_body.dump());
// 解析响应
return parseEmbeddingResponse(response);
}
3.3 sendPostRequest():使用 libcurl 发起 HTTP POST
cpp
std::string EmbeddingService::sendPostRequest(const std::string& data) {
CURL* curl = curl_easy_init();
std::string response_data;
// ★ 设置请求头
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
std::string auth_header = "Authorization: Bearer " + config_.api_key;
headers = curl_slist_append(headers, auth_header.c_str());
// 配置 CURL
curl_easy_setopt(curl, CURLOPT_URL, config_.api_url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, config_.timeout_ms);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
}
return response_data;
}
DashScope API 请求/响应格式:
json
// 请求
{
"model": "text-embedding-v2",
"input": { "texts": ["计算 123 + 456 的结果"] },
"parameters": { "text_type": "query" }
}
// 响应
{
"output": {
"embeddings": [
{
"text_index": 0,
"embedding": [0.0234, -0.0156, 0.0421, ...] // 1536 维浮点数组
}
]
}
}
3.4 指数退避重试
cpp
std::string EmbeddingService::callApiWithRetry(const std::string& request_body) {
last_retry_stats_ = RetryStats{};
std::exception_ptr last_exception;
for (int attempt = 0; attempt <= config_.max_retries; ++attempt) {
last_retry_stats_.total_attempts++;
if (attempt > 0) {
// ★ 指数退避:delay = initial_delay × 2^(attempt-1) + 随机抖动
int delay_ms = calculateBackoffDelay(attempt);
last_retry_stats_.retry_delays_ms.push_back(delay_ms);
// 调用回调(用于测试时注入)
if (retry_callback_) {
retry_callback_(attempt, delay_ms);
}
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
}
try {
std::string response = sendPostRequest(request_body);
last_retry_stats_.successful_attempts++;
return response;
} catch (const std::exception& e) {
last_retry_stats_.failed_attempts++;
last_exception = std::current_exception();
}
}
// 所有重试都失败
if (last_exception) std::rethrow_exception(last_exception);
throw std::runtime_error("Embedding API call failed after all retries");
}
int EmbeddingService::calculateBackoffDelay(int attempt) const {
// ★ 指数退避公式: delay = initial_delay × 2^(attempt-1)
int base_delay = config_.initial_retry_delay_ms * (1 << (attempt - 1));
// 加上 0-25% 的随机抖动,避免多客户端同时重试
int jitter = (std::rand() % (base_delay / 4 + 1));
return base_delay + jitter;
}
重试延迟示例 (initial_retry_delay_ms = 1000):
| 重试次数 | 基础延迟 | 加抖动后范围 |
|---|---|---|
| 第 1 次 | 1000ms | 1000~1250ms |
| 第 2 次 | 2000ms | 2000~2500ms |
| 第 3 次 | 4000ms | 4000~5000ms |
3.5 余弦相似度计算
cpp
float EmbeddingService::cosineSimilarity(
const std::vector<float>& a,
const std::vector<float>& b) {
if (a.size() != b.size() || a.empty()) return 0.0f;
float dot_product = 0.0f; // 点积: Σ(a_i × b_i)
float norm_a = 0.0f; // a 的模: Σ(a_i²)
float norm_b = 0.0f; // b 的模: Σ(b_i²)
for (size_t i = 0; i < a.size(); ++i) {
dot_product += a[i] * b[i];
norm_a += a[i] * a[i];
norm_b += b[i] * b[i];
}
if (norm_a == 0.0f || norm_b == 0.0f) return 0.0f;
// a · b
// cos = ─────────
// |a| × |b|
return dot_product / (std::sqrt(norm_a) * std::sqrt(norm_b));
}
余弦相似度的含义:
cosine_similarity ( a ⃗ , b ⃗ ) = a ⃗ ⋅ b ⃗ ∣ a ⃗ ∣ × ∣ b ⃗ ∣ \text{cosine\_similarity}(\vec{a}, \vec{b}) = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| \times |\vec{b}|} cosine_similarity(a ,b )=∣a ∣×∣b ∣a ⋅b
- 值域: [ − 1 , 1 ] [-1, 1] [−1,1]
- 1 1 1 = 完全相同方向(语义最相似)
- 0 0 0 = 正交(无关)
- − 1 -1 −1 = 完全相反方向
3.6 解析 API 响应
cpp
std::vector<std::vector<float>> EmbeddingService::parseEmbeddingResponse(
const std::string& response) {
json response_json = json::parse(response);
// 检查 API 错误
if (response_json.contains("code")) {
throw std::runtime_error("DashScope API Error: " +
response_json.value("message", "Unknown error"));
}
// 提取 embeddings
std::vector<std::vector<float>> results;
if (response_json.contains("output") &&
response_json["output"].contains("embeddings")) {
for (const auto& item : response_json["output"]["embeddings"]) {
if (item.contains("embedding")) {
std::vector<float> embedding;
for (const auto& val : item["embedding"]) {
embedding.push_back(val.get<float>());
}
results.push_back(embedding);
}
}
}
if (results.empty()) throw std::runtime_error("No embeddings in response");
return results;
}
四、VectorIndex:内存向量索引
4.1 数据结构
cpp
// 索引中的每条工具记录
struct IndexedTool {
std::string name; // 工具名:"calculator"
std::string description; // "Evaluates a mathematical expression"
std::string input_schema; // JSON Schema 字符串
std::vector<float> embedding; // ★ 1536 维向量
int64_t created_at = 0; // 创建时间戳
int64_t updated_at = 0; // 更新时间戳
};
// 搜索结果
struct SearchResult {
IndexedTool tool; // 工具信息
float similarity; // 余弦相似度分数 [0, 1]
};
class VectorIndex {
private:
std::unordered_map<std::string, IndexedTool> tools_; // name → IndexedTool
mutable std::mutex mutex_; // 线程安全
std::string version_ = "1.0";
};
4.2 search():Top-K 相似度搜索(核心)
cpp
std::vector<SearchResult> VectorIndex::search(
const std::vector<float>& query_embedding,
int top_k,
float threshold) const {
std::lock_guard<std::mutex> lock(mutex_); // ★ 线程安全
if (tools_.empty() || query_embedding.empty()) return {};
// ═══════════════════════════════════════════════
// 第 ① 步:计算查询向量与每个工具向量的余弦相似度
// ═══════════════════════════════════════════════
std::vector<SearchResult> all_results;
all_results.reserve(tools_.size());
for (const auto& pair : tools_) {
const IndexedTool& tool = pair.second;
if (tool.embedding.empty()) continue;
float similarity = cosineSimilarity(query_embedding, tool.embedding);
SearchResult result;
result.tool = tool;
result.similarity = similarity;
all_results.push_back(result);
}
// ═══════════════════════════════════════════════
// 第 ② 步:按相似度降序排序
// ═══════════════════════════════════════════════
std::sort(all_results.begin(), all_results.end(),
[](const SearchResult& a, const SearchResult& b) {
return a.similarity > b.similarity; // 降序
});
// ═══════════════════════════════════════════════
// 第 ③ 步:应用阈值过滤
// ═══════════════════════════════════════════════
std::vector<SearchResult> filtered_results;
for (const auto& result : all_results) {
if (result.similarity >= threshold) {
filtered_results.push_back(result);
}
}
// ★ 降级策略:如果没有工具超过阈值,至少返回最佳匹配
if (filtered_results.empty() && !all_results.empty()) {
filtered_results.push_back(all_results[0]);
}
// ═══════════════════════════════════════════════
// 第 ④ 步:截取 Top-K
// ═══════════════════════════════════════════════
if (static_cast<int>(filtered_results.size()) > top_k) {
filtered_results.resize(top_k);
}
return filtered_results;
}
搜索流程图:
查询向量 (1536 维)
│
├── 与 tool_0.embedding 计算 cos = 0.82
├── 与 tool_1.embedding 计算 cos = 0.45
├── 与 tool_2.embedding 计算 cos = 0.91 ← 最高
├── 与 tool_3.embedding 计算 cos = 0.12
├── 与 tool_4.embedding 计算 cos = 0.67
... (遍历所有工具)
│
▼ 排序 + 过滤(threshold=0.3) + 截取(top_k=3)
│
├── tool_2 (cos=0.91)
├── tool_0 (cos=0.82)
└── tool_4 (cos=0.67)
4.3 持久化:保存/加载 JSON 文件
cpp
bool VectorIndex::saveToFile(const std::string& path) const {
std::lock_guard<std::mutex> lock(mutex_);
json tools_array = json::array();
for (const auto& pair : tools_) {
const IndexedTool& tool = pair.second;
json tool_json = {
{"name", tool.name},
{"description", tool.description},
{"input_schema", tool.input_schema},
{"embedding", tool.embedding}, // ★ 1536 个浮点数存入 JSON
{"created_at", tool.created_at},
{"updated_at", tool.updated_at}
};
tools_array.push_back(tool_json);
}
json index_json = {
{"version", version_},
{"model", "text-embedding-v2"},
{"dimension", 1536},
{"tools", tools_array}
};
std::ofstream file(path);
file << index_json.dump(2); // 格式化输出
file.close();
return true;
}
bool VectorIndex::loadFromFile(const std::string& path) {
std::lock_guard<std::mutex> lock(mutex_);
std::ifstream file(path);
json index_json = json::parse(file);
tools_.clear();
for (const auto& tool_json : index_json["tools"]) {
IndexedTool tool;
tool.name = tool_json.value("name", "");
tool.description = tool_json.value("description", "");
tool.input_schema = tool_json.value("input_schema", "");
tool.created_at = tool_json.value("created_at", 0);
tool.updated_at = tool_json.value("updated_at", 0);
for (const auto& val : tool_json["embedding"]) {
tool.embedding.push_back(val.get<float>());
}
tools_[tool.name] = tool;
}
return true;
}
持久化的意义:避免每次重启都重新调用 Embedding API(有成本、有延迟),索引文件可以直接加载。
五、EmbeddingCache:LRU 缓存
5.1 为什么需要缓存?
同一个查询文本(如 "计算 123 + 456")可能被多次发送到 Embedding API。缓存可以:
- 减少 API 调用次数(DashScope Embedding 按调用次数计费)
- 降低延迟(缓存命中时直接返回,不用等 HTTP 往返)
- 减轻 API 限流压力
5.2 数据结构:HashMap + 双向链表
cpp
class EmbeddingCache {
private:
struct CacheEntry {
std::vector<float> embedding; // 缓存的向量
std::chrono::steady_clock::time_point created_at; // 创建时间(用于 TTL 过期)
};
// ★ 经典 LRU 数据结构
using CacheList = std::list<std::pair<std::string, CacheEntry>>; // 双向链表
using CacheMap = std::unordered_map<std::string, CacheList::iterator>; // 哈希表
CacheList cache_list_; // 链表头部 = 最近使用,尾部 = 最久未使用
CacheMap cache_map_; // text → 链表节点迭代器(O(1) 查找)
mutable std::mutex mutex_;
};
LRU 原理图:
cache_list_ (双向链表):
HEAD ← 最近使用 最久未使用 → TAIL
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ "天气" │←→│ "计算" │←→│ "搜索" │←→│ "翻译" │
│ vec_1 │ │ vec_2 │ │ vec_3 │ │ vec_4 │
└────────┘ └────────┘ └────────┘ └────────┘
cache_map_ (哈希表): O(1) 定位
"天气" → 指向链表节点 1
"计算" → 指向链表节点 2
"搜索" → 指向链表节点 3
"翻译" → 指向链表节点 4
5.3 get():获取缓存(带过期检查)
cpp
std::optional<std::vector<float>> EmbeddingCache::get(const std::string& text) {
if (!config_.enabled) {
stats_.misses++;
return std::nullopt;
}
std::lock_guard<std::mutex> lock(mutex_);
// ① HashMap O(1) 查找
auto it = cache_map_.find(text);
if (it == cache_map_.end()) {
stats_.misses++;
return std::nullopt; // 未命中
}
// ② 检查是否过期(TTL)
if (isExpired(it->second->second)) {
cache_list_.erase(it->second);
cache_map_.erase(it);
stats_.misses++;
return std::nullopt; // 已过期,视为未命中
}
// ③ ★ 命中:移到链表头部(标记为最近使用)
moveToFront(it);
stats_.hits++;
return it->second->second.embedding; // 返回缓存的向量
}
5.4 put():存入缓存
cpp
void EmbeddingCache::put(const std::string& text, const std::vector<float>& embedding) {
if (!config_.enabled) return;
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_map_.find(text);
if (it != cache_map_.end()) {
// 已存在 → 更新值 + 移到头部
it->second->second.embedding = embedding;
it->second->second.created_at = std::chrono::steady_clock::now();
moveToFront(it);
return;
}
// ★ 缓存已满 → 驱逐最久未使用的(链表尾部)
while (cache_list_.size() >= config_.max_size && !cache_list_.empty()) {
evictLRU();
}
// 添加到链表头部
CacheEntry entry;
entry.embedding = embedding;
entry.created_at = std::chrono::steady_clock::now();
cache_list_.push_front({text, entry});
cache_map_[text] = cache_list_.begin();
}
5.5 LRU 驱逐
cpp
void EmbeddingCache::evictLRU() {
if (cache_list_.empty()) return;
// ★ 移除链表尾部(最久未使用的条目)
auto& back = cache_list_.back();
last_evicted_key_ = back.first;
cache_map_.erase(back.first); // 从 HashMap 中删除
cache_list_.pop_back(); // 从链表中删除
stats_.evictions++;
}
5.6 TTL 过期检查
cpp
bool EmbeddingCache::isExpired(const CacheEntry& entry) const {
if (config_.ttl_seconds <= 0) return false; // TTL=0 表示永不过期
auto now = std::chrono::steady_clock::now();
auto age = std::chrono::duration_cast<std::chrono::seconds>(
now - entry.created_at).count();
return age >= config_.ttl_seconds; // 超过 ttl_seconds(默认 3600 秒 = 1 小时) 则过期
}
5.7 moveToFront:O(1) 移到头部
cpp
void EmbeddingCache::moveToFront(CacheMap::iterator it) {
// ★ std::list::splice 是 O(1) 操作
cache_list_.splice(cache_list_.begin(), cache_list_, it->second);
}
std::list::splice 不会拷贝节点,只是修改指针,所以是常数时间。
六、ToolRetriever:检索器(整合三大组件)
ToolRetriever 是门面类(Facade) ,将 EmbeddingService、EmbeddingCache、VectorIndex 组合在一起,提供两大核心操作:indexTools()(建索引)和 retrieve()(检索)。
6.1 配置
cpp
struct RetrieverConfig {
EmbeddingConfig embedding_config; // Embedding 服务配置
CacheConfig cache_config; // 缓存配置
int top_k = 5; // 返回前 K 个最相似的工具
float similarity_threshold = 0.3f; // 相似度阈值(低于此值的结果被过滤)
bool enable_validation = false; // 是否验证工具可用性
std::string index_path; // 索引持久化文件路径
bool auto_save_index = true; // 关闭时自动保存索引
};
6.2 initialize():初始化三大组件
cpp
bool ToolRetriever::initialize() {
// 创建 Embedding 服务(封装 DashScope API 调用)
embedding_service_ = std::make_unique<EmbeddingService>(config_.embedding_config);
// 创建 LRU 缓存
cache_ = std::make_unique<EmbeddingCache>(config_.cache_config);
// 创建向量索引
index_ = std::make_unique<VectorIndex>();
// ★ 尝试从文件加载已有的索引(避免重新调 Embedding API)
if (!config_.index_path.empty()) {
if (!index_->loadFromFile(config_.index_path)) {
LOG_INFO("Creating new vector index"); // 文件不存在则创建新索引
}
}
initialized_ = true;
return true;
}
6.3 buildToolText():将工具信息组合成文本(简历第(1)点的核心)
cpp
std::string ToolRetriever::buildToolText(const ToolInfo& tool) {
std::ostringstream oss;
// ★ 将工具名称 + 描述 + 参数信息组合成一段自然语言文本
oss << "Tool: " << tool.name << "\n";
oss << "Description: " << tool.description << "\n";
// 简化 input_schema 用于向量化(只提取参数名和描述)
if (!tool.input_schema.empty()) {
try {
json schema = json::parse(tool.input_schema);
if (schema.contains("properties")) {
oss << "Parameters: ";
for (auto& [key, value] : schema["properties"].items()) {
oss << key;
if (value.contains("description")) {
oss << " (" << value["description"].get<std::string>() << ")";
}
oss << ", ";
}
}
} catch (...) {
// 忽略解析错误
}
}
return oss.str();
}
示例 :对于 calculator 插件的 add 工具,生成的文本是:
Tool: add
Description: Adds two numbers
Parameters: a (first number), b (second number),
这段文本被发给 DashScope Embedding API,生成 1536 维向量。
6.4 addTool() / indexTools():建索引(简历第(1)点)
cpp
void ToolRetriever::addTool(const ToolInfo& tool) {
// ① 组合工具信息为文本
std::string tool_text = buildToolText(tool);
// ② 调用 Embedding API 生成向量(带缓存)
std::vector<float> embedding = getEmbedding(tool_text);
// ③ 存入向量索引
IndexedTool indexed_tool;
indexed_tool.name = tool.name;
indexed_tool.description = tool.description;
indexed_tool.input_schema = tool.input_schema;
indexed_tool.embedding = embedding;
index_->addTool(indexed_tool);
}
void ToolRetriever::indexTools(const std::vector<ToolInfo>& tools) {
for (const auto& tool : tools) {
addTool(tool); // 逐个建索引
}
}
建索引完整流程:
tools/list 获取全部工具 (例如 30 个)
│
▼
逐个处理:
│
├── buildToolText(tool) → "Tool: calculator\nDescription: ...\nParameters: ..."
│ │
│ ▼
├── getEmbedding(text)
│ │
│ ├── cache_.get(text) → 命中?直接返回向量
│ │
│ └── (未命中) embedding_service_.embed(text) → curl POST DashScope API
│ │ │
│ └── cache_.put(text, embedding) │
│ │
│ ◄───── 返回 1536 维向量 ◄─────────────────────────┘
│
└── index_->addTool({name, desc, schema, embedding})
│
└── tools_["calculator"] = IndexedTool{..., embedding}
6.5 retrieve():智能检索(简历第(2)点的核心)
cpp
std::vector<RetrievedTool> ToolRetriever::retrieve(const std::string& query, int top_k) {
if (index_->size() == 0) return {};
// ① 将用户查询转为向量
std::vector<float> query_embedding = getEmbedding(query);
// ② 在索引中搜索最相似的 K 个工具
auto search_results = index_->search(
query_embedding,
top_k, // 返回前 K 个
config_.similarity_threshold); // 过滤低于阈值的
// ③ 转换为 RetrievedTool 格式
std::vector<RetrievedTool> results;
results.reserve(search_results.size());
for (const auto& sr : search_results) {
RetrievedTool tool;
tool.name = sr.tool.name;
tool.description = sr.tool.description;
tool.input_schema = sr.tool.input_schema;
tool.relevance_score = sr.similarity; // 带上相关性分数
results.push_back(tool);
}
return results;
}
6.6 getEmbedding():带缓存的向量获取
cpp
std::vector<float> ToolRetriever::getEmbedding(const std::string& text) {
// ★ 先查缓存
if (cache_) {
auto cached = cache_->get(text);
if (cached.has_value()) {
return cached.value(); // 缓存命中,直接返回
}
}
// ★ 缓存未命中 → 调用 API
std::vector<float> embedding = embedding_service_->embed(text);
// ★ 存入缓存
if (cache_) {
cache_->put(text, embedding);
}
return embedding;
}
6.7 toFunctionCallingFormat():转换为 LLM 函数调用格式
cpp
std::string ToolRetriever::toFunctionCallingFormat(const std::vector<RetrievedTool>& tools) {
json functions = json::array();
for (const auto& tool : tools) {
json func = {
{"name", tool.name},
{"description", tool.description}
};
// 解析 input_schema
if (!tool.input_schema.empty()) {
try {
func["parameters"] = json::parse(tool.input_schema);
} catch (...) {
func["parameters"] = json::object();
}
}
functions.push_back(func);
}
return functions.dump(2);
}
输出的 JSON 可直接放入 LLM 的 function calling / tool use prompt 中:
json
[
{
"name": "calculator",
"description": "Evaluates a mathematical expression",
"parameters": {
"type": "object",
"properties": {
"expression": { "type": "string", "description": "Math expression" }
},
"required": ["expression"]
}
},
{
"name": "add",
"description": "Adds two numbers",
"parameters": { ... }
}
]
七、MCPAgentIntegration:统一集成入口与降级机制
7.1 RAG 初始化流程
cpp
bool MCPAgentIntegration::initializeRAG() {
// 构建 RetrieverConfig(从 RAGConfig 映射)
rag::RetrieverConfig retriever_config;
retriever_config.embedding_config.api_key = config_.rag_config.api_key;
if (retriever_config.embedding_config.api_key.empty()) {
retriever_config.embedding_config.loadApiKeyFromEnv(); // 从 DASHSCOPE_API_KEY 读取
}
retriever_config.embedding_config.model = config_.rag_config.model;
retriever_config.top_k = config_.rag_config.top_k;
retriever_config.similarity_threshold = config_.rag_config.similarity_threshold;
retriever_config.index_path = config_.rag_config.index_path;
retriever_config.cache_config.enabled = config_.rag_config.enable_cache;
retriever_config.cache_config.max_size = config_.rag_config.cache_max_size;
retriever_config.cache_config.ttl_seconds = config_.rag_config.cache_ttl_seconds;
// 创建并初始化 ToolRetriever
tool_retriever_ = std::make_unique<rag::ToolRetriever>(retriever_config);
if (!tool_retriever_->initialize()) {
tool_retriever_.reset();
return false;
}
// ★ 索引现有所有工具
if (!tool_cache_.empty()) {
tool_retriever_->indexTools(tool_cache_);
}
rag_initialized_ = true;
return true;
}
7.2 getRelevantTools():对外接口(含降级)
cpp
std::vector<ToolInfo> MCPAgentIntegration::getRelevantTools(
const std::string& query, int top_k) const {
// ★ 降级策略:RAG 未启用时,返回所有工具(传统模式)
if (!isRAGEnabled()) {
return getAvailableTools();
}
try {
auto retrieved = tool_retriever_->retrieve(query, top_k);
std::vector<ToolInfo> result;
for (const auto& rt : retrieved) {
ToolInfo info;
info.name = rt.name;
info.description = rt.description;
info.input_schema = rt.input_schema;
result.push_back(info);
}
return result;
} catch (const std::exception& e) {
// ★ 降级策略:RAG 检索失败时,回退到返回所有工具
LOG_ERROR("RAG retrieval failed, falling back to all tools: " + std::string(e.what()));
return getAvailableTools();
}
}
两层降级保护:
- RAG 未启用(
!isRAGEnabled())→ 返回全部工具 - RAG 检索异常(DashScope API 故障)→
catch捕获,回退到全部工具
这确保了 RAG 组件不会成为系统的单点故障。
7.3 getRelevantToolsAsJson():一步到位
cpp
std::string MCPAgentIntegration::getRelevantToolsAsJson(const std::string& query) const {
auto tools = getRelevantTools(query); // 智能检索
return toFunctionCallingFormat(tools); // 转为 LLM 可用的 JSON
}
这个方法是给 AI Agent 调用的终极接口------传入用户查询,直接得到可以放入 LLM prompt 的 JSON 函数定义。
八、完整调用流程:从用户查询到 LLM 工具选择
步骤 1: 用户输入
──────────────────────
"帮我查一下北京今天的天气"
│
▼
步骤 2: AI Agent 调用 getRelevantTools("帮我查一下北京今天的天气")
──────────────────────
MCPAgentIntegration::getRelevantTools()
│
│ isRAGEnabled()? → Yes
▼
步骤 3: ToolRetriever::retrieve("帮我查一下北京今天的天气", top_k=5)
──────────────────────
│
├── getEmbedding("帮我查一下北京今天的天气")
│ │
│ ├── cache_.get("帮我查一下北京今天的天气") → 未命中
│ │
│ └── embedding_service_->embed("帮我查一下北京今天的天气")
│ │
│ └── curl POST https://dashscope.aliyuncs.com/...
│ │
│ └── 返回 1536 维向量 query_vec
│
│ cache_.put("帮我查一下北京今天的天气", query_vec)
│
▼
步骤 4: VectorIndex::search(query_vec, top_k=5, threshold=0.3)
──────────────────────
│
├── cos(query_vec, "get_weather" 的向量) = 0.89 ← 最高!
├── cos(query_vec, "calculator" 的向量) = 0.15
├── cos(query_vec, "add" 的向量) = 0.12
├── cos(query_vec, "sleep" 的向量) = 0.08
├── cos(query_vec, "code_review" 的向量) = 0.22
...
│
▼ 排序 + 过滤(>=0.3) + Top-5
│
└── 结果: [get_weather (0.89)] ← 只有 1 个超过阈值
│
▼
步骤 5: toFunctionCallingFormat()
──────────────────────
[{
"name": "get_weather",
"description": "Get weather forecast of a city...",
"parameters": {
"type": "object",
"properties": {
"latitude": { "type": "string" },
"longitude": { "type": "string" },
"city": { "type": "string" }
},
"required": ["city", "latitude", "longitude"]
}
}]
│
▼
步骤 6: 发送给 LLM(如通义千问)
──────────────────────
prompt = "用户问:帮我查一下北京今天的天气\n可用工具:" + functions_json
│
▼
LLM 选择调用 get_weather(latitude="39.9", longitude="116.4", city="Beijing")
│
▼
步骤 7: callTool("get_weather", arguments) → Weather 插件执行 → 返回天气预报
效果对比:
- 传统方式:把 30+ 个工具的 JSON Schema(~15000 token)全部塞进 prompt
- RAG-MCP 方式:只把 1~5 个最相关的工具(~500 token)发给 LLM
九、ToolValidator:可选的工具验证
ToolValidator 在 ToolRetriever 检索出工具后,可以对检索到的工具进行可用性验证------确保工具确实能被调用。
9.1 验证流程
cpp
ValidationResult ToolValidator::validate(const RetrievedTool& tool) {
// ① 生成测试查询(根据 JSON Schema 自动推断参数)
auto test_queries = generateTestQueries(tool);
// ② 异步执行测试查询(带超时保护)
for (const auto& query : test_queries) {
auto future = std::async(std::launch::async, [this, &tool, &query]() {
return executeTestQuery(tool.name, query);
});
auto status = future.wait_for(std::chrono::milliseconds(config_.timeout_ms));
if (status == std::future_status::timeout) {
// 超时处理:根据配置决定是否视为有效
result.is_valid = config_.treat_timeout_as_valid;
break;
}
}
}
9.2 自动生成测试参数
cpp
std::vector<std::string> ToolValidator::generateTestQueries(const RetrievedTool& tool) {
json schema = json::parse(tool.input_schema);
json test_params = json::object();
for (auto& [key, value] : schema["properties"].items()) {
std::string type = value.value("type", "string");
// ★ 根据类型自动填充测试值
if (type == "string") test_params[key] = "test";
else if (type == "number") test_params[key] = 1;
else if (type == "boolean") test_params[key] = true;
else if (type == "array") test_params[key] = json::array();
else if (type == "object") test_params[key] = json::object();
}
return {test_params.dump()};
}
十、属性测试(RapidCheck)
项目使用 RapidCheck 进行属性测试,验证 RAG-MCP 的数学不变量:
10.1 搜索结果排序性
cpp
RC_GTEST_PROP(VectorIndexProperties, SearchResultsOrdering, ()) {
VectorIndex index;
int num_tools = *rc::gen::inRange(3, 20);
int dimension = 128;
// 添加随机工具
for (int i = 0; i < num_tools; ++i) {
IndexedTool tool;
tool.name = generateToolName(i);
tool.embedding = generateRandomVector(dimension);
index.addTool(tool);
}
auto query = generateRandomVector(dimension);
int top_k = *rc::gen::inRange(1, num_tools + 1);
auto results = index.search(query, top_k);
// ★ 属性:结果必须按相似度降序排列
for (size_t i = 1; i < results.size(); ++i) {
RC_ASSERT(results[i-1].similarity >= results[i].similarity);
}
}
10.2 Top-K 数量
cpp
RC_GTEST_PROP(VectorIndexProperties, TopKResultCount, ()) {
// ... setup ...
auto results = index.search(query, top_k, -1.0f);
// ★ 属性:返回数量 = min(top_k, num_tools)
int expected = std::min(top_k, num_tools);
RC_ASSERT(static_cast<int>(results.size()) == expected);
}
10.3 持久化 Round-Trip
cpp
RC_GTEST_PROP(VectorIndexProperties, IndexPersistenceRoundTrip, ()) {
// ... 创建索引,添加工具 ...
original_index.saveToFile(temp_path);
VectorIndex loaded_index;
loaded_index.loadFromFile(temp_path);
// ★ 属性:保存后加载,大小不变
RC_ASSERT(original_index.size() == loaded_index.size());
}
十一、设计模式分析
| 模式 | 应用位置 | 说明 |
|---|---|---|
| 门面模式(Facade) | ToolRetriever |
将 EmbeddingService + Cache + VectorIndex 组合为简单接口 |
| 策略模式 | EmbeddingConfig.model |
可切换不同 Embedding 模型 |
| 降级模式 | getRelevantTools() |
RAG 不可用时自动回退全量工具 |
| 缓存模式 + LRU | EmbeddingCache |
HashMap + 双向链表实现 O(1) LRU |
| 指数退避重试 | callApiWithRetry() |
d e l a y = i n i t i a l × 2 ( a t t e m p t − 1 ) + j i t t e r delay = initial \times 2^{(attempt-1)} + jitter delay=initial×2(attempt−1)+jitter |
| Pimpl / 组合 | ToolRetriever 持有 unique_ptr 成员 |
隐藏实现细节 |
十二、面试话术
面试官:你的 RAG-MCP 智能工具选择是怎么实现的?
分两步:建索引 和检索。
建索引:当 MCP Server 启动后,我从它获取所有注册的工具列表。对每个工具,我把它的名称、描述和参数信息(从 JSON Schema 中提取参数名和描述)组合成一段自然语言文本,然后调用阿里 DashScope 的 text-embedding-v2 模型 API,将这段文本转换为 1536 维的浮点数向量,存储在一个内存向量索引中。索引可以持久化为 JSON 文件,避免重启后重新调 API。
检索:当用户发来查询(如 "帮我查北京天气"),我同样将查询文本通过 DashScope API 转为向量,然后在向量索引中计算查询向量与每个工具向量的余弦相似度,按相似度降序排序,过滤掉低于阈值(默认 0.3)的结果,只保留前 K 个(默认 5 个),将这些最相关的工具转换为 LLM 的 function calling 格式,发送给大模型。LLM 只需要从几个候选工具中选择,而不是从全部几十个里选。
另外我加了一个 LRU 缓存层------用 HashMap + 双向链表实现,O(1) 查找和更新。同样的查询文本不会重复调 API,缓存支持 TTL 过期和容量限制。
面试官:余弦相似度为什么适合衡量文本语义相似性?余弦相似度衡量的是两个向量的方向 是否一致,而不关心长度(模长)。Embedding 模型输出的向量,语义相近的文本会被映射到向量空间中相近的方向。比如 "天气查询" 和 "查北京今天天气" 生成的向量方向接近(余弦相似度高),而 "天气查询" 和 "计算 2+3" 方向差异大(余弦相似度低)。
公式是: cos ( θ ) = a ⃗ ⋅ b ⃗ ∣ a ⃗ ∣ × ∣ b ⃗ ∣ \cos(\theta) = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| \times |\vec{b}|} cos(θ)=∣a ∣×∣b ∣a ⋅b ,值域 [-1, 1],1 表示完全同向,0 表示正交。
面试官:如果 DashScope API 挂了怎么办?有三层保护。第一层是指数退避重试 ------API 调用失败后,依次等待 1 秒、2 秒、4 秒再重试,最多重试 3 次,加上随机抖动避免多客户端同时重试。第二层是 LRU 缓存 ------如果之前相同的查询已经成功调用过 API,缓存命中就不需要再调。第三层是降级策略 ------如果 RAG 检索整体失败,
getRelevantTools()的 catch 块会捕获异常,回退到返回全部工具的传统模式,保证系统依然可用。
面试官:你的 LRU 缓存是怎么实现 O(1) 的?用
std::unordered_map+std::list的经典组合。unordered_map的键是文本字符串,值是list迭代器------提供 O(1) 查找。list的头部是最近使用的条目,尾部是最久未使用的。get()命中时,用list::splice()将节点移到头部(O(1) 指针操作)。put()时如果缓存满了,就从尾部删除最久未使用的条目(pop_back())。这样 get/put/evict 都是 O(1)。
面试官:你的向量搜索是暴力遍历,工具多了不会慢吗?当前实现确实是 O(n) 暴力遍历------遍历所有工具计算余弦相似度再排序。对于 MCP 场景,工具数量通常在几十到几百的规模,O(n) 完全够用(几百个 1536 维向量的余弦相似度计算在毫秒级完成)。如果工具扩展到上万级别,可以引入近似最近邻(ANN)算法如 HNSW(Hierarchical Navigable Small World)或使用 FAISS 库。但在当前规模下,暴力搜索的优势是实现简单、结果精确、没有额外依赖。
功能测试:LLM + MCP 协议集成三方工具的端到端流程
简历原文:功能测试:调用阿里百炼的大模型,使用开发的mcp协议接口进行集成三方工具,进行问题询问,看到成功检索对应工具进行加载,并利用三方工具进行问题解答
一、端到端流程总览
这个简历条目描述的是一个端到端的集成验证流程:
用户提问 "计算 123 + 456"
│
▼
┌─────────────┐
│ AI Agent │ (MathAgent / Orchestrator)
│ 接收问题 │
└──────┬──────┘
│
▼
┌─────────────┐ ┌───────────────────┐
│ MCP 协议接口 │────→│ MCP Server │
│ (MCPClient) │←───│ (三方工具宿主) │
└──────┬──────┘ │ │
│ │ ★ 动态加载插件 │
│ │ calculator.so │
│ │ weather.so │
│ │ code_review.so │
│ └───────────────────┘
▼
┌─────────────────┐
│ 工具检索 (RAG) │ ← 用 DashScope Embedding
│ 找到 calculator │ 语义检索最相关的工具
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 调用通义千问 LLM │ ← DashScope chat API
│ 将工具结果 + │
│ 用户问题一起发送 │
└──────┬──────────┘
│
▼
返回给用户:"123 + 456 = 579"
涉及的核心组件和文件:
| 组件 | 文件 | 职责 |
|---|---|---|
| QwenClient | a2a/include/a2a/examples/qwen_client.hpp | 调用阿里百炼通义千问 API |
| MCPAgentIntegration | mcp/src/mcp_agent_integration.cpp | MCP 协议接口:连接 Server、获取工具、调用工具、RAG检索 |
| MCPClient | mcp/include/agent_rpc/mcp/mcp_client.h | MCP 客户端:STDIO 通信,JSON-RPC 协议 |
| MCPToolManager | mcp/include/agent_rpc/mcp/mcp_client.h | 工具管理器:工具发现、参数验证、执行调度 |
| MathAgent | examples/ai_orchestrator/math_agent_main.cpp | 数学专家 Agent:集成 LLM + MCP + RAG |
| AIOrchestrator | examples/ai_orchestrator/orchestrator_main.cpp | 调度 Agent:意图识别 + 路由 + MCP 工具增强 |
| 启动脚本 | examples/ai_orchestrator/start_system.sh | 一键启动整个系统 |
二、QwenClient:调用阿里百炼大模型
2.1 类定义
cpp
class QwenClient {
public:
explicit QwenClient(const std::string& api_key,
const std::string& model = "qwen-plus")
: api_key_(api_key)
, model_(model)
, api_url_("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation") {
curl_global_init(CURL_GLOBAL_DEFAULT);
}
private:
std::string api_key_; // 阿里百炼 API Key
std::string model_; // 模型名:"qwen-plus"
std::string api_url_; // DashScope 文本生成 API
};
2.2 chat():核心对话方法
cpp
std::string chat(const std::string& system_prompt,
const std::string& user_message) {
// ★ 构造 DashScope 文本生成 API 请求
json request_body = {
{"model", model_}, // "qwen-plus"
{"input", {
{"messages", json::array({
{{"role", "system"}, {"content", system_prompt}}, // 系统提示词
{{"role", "user"}, {"content", user_message}} // 用户消息
})}
}},
{"parameters", {
{"result_format", "message"} // 返回格式
}}
};
// 发起 HTTP POST 请求
std::string response = send_post_request(request_body.dump());
// 解析响应
json response_json = json::parse(response);
// 检查 API 错误
if (response_json.contains("code")) {
throw std::runtime_error("API Error: " +
response_json.value("message", "Unknown error"));
}
// ★ 提取 AI 回复内容
// 路径: response["output"]["choices"][0]["message"]["content"]
return response_json["output"]["choices"][0]["message"]["content"].get<std::string>();
}
2.3 HTTP 请求发送
cpp
std::string send_post_request(const std::string& data) {
CURL* curl = curl_easy_init();
std::string response_data;
// 设置请求头(与 EmbeddingService 类似)
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
std::string auth_header = "Authorization: Bearer " + api_key_;
headers = curl_slist_append(headers, auth_header.c_str());
// 配置 CURL
curl_easy_setopt(curl, CURLOPT_URL, api_url_.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // 30 秒超时
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
}
return response_data;
}
DashScope Chat API 请求/响应示例:
json
// 请求
POST https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation
{
"model": "qwen-plus",
"input": {
"messages": [
{"role": "system", "content": "你是数学助手。工具计算结果:579"},
{"role": "user", "content": "计算 123 + 456"}
]
},
"parameters": {"result_format": "message"}
}
// 响应
{
"output": {
"choices": [{
"message": {
"role": "assistant",
"content": "123 + 456 = 579\n\n计算过程:..."
}
}]
}
}
三、MCPAgentIntegration:MCP 协议集成入口
这是连接 AI Agent 与 MCP Server 的桥梁类,提供完整的工具生命周期管理。
3.1 初始化流程:连接 MCP Server + 发现工具 + 启用 RAG
cpp
bool MCPAgentIntegration::initialize(const MCPAgentConfig& config) {
config_ = config;
if (!config_.enable_mcp) {
initialized_ = true; // MCP 未启用,直接返回
return true;
}
if (config_.mcp_server_path.empty()) {
initialized_ = true; // 路径为空,降级模式
return true;
}
// ★ 第 ① 步:连接 MCP Server(启动子进程 + 建立 STDIO 通信)
if (!connectToMCPServer()) {
// 连接失败,降级模式(不crash,只是 MCP 不可用)
initialized_ = true;
return true;
}
// ★ 第 ② 步:发现工具(通过 tools/list 获取所有注册的工具)
updateToolCache();
// ★ 第 ③ 步:如果启用 RAG,初始化智能工具检索
if (config_.rag_config.enabled) {
initializeRAG(); // 对所有工具建 Embedding 向量索引
}
initialized_ = true;
return true;
}
3.2 connectToMCPServer():建立与 MCP Server 的连接
cpp
bool MCPAgentIntegration::connectToMCPServer() {
// ① 创建 MCP Client(STDIO 通信)
mcp_client_ = std::make_shared<MCPClient>();
// ② 启动 MCP Server 子进程并通过 STDIO 管道建立连接
// mcp_server_path 例如: "/path/to/mcp_server"
// mcp_args 例如: ["-n", "server", "-l", "/tmp/logs", "-p", "/path/to/plugins"]
if (!mcp_client_->connect(config_.mcp_server_path, config_.mcp_args)) {
mcp_client_.reset();
return false;
}
// ③ 创建工具管理器(封装 tools/list 和 tools/call 协议)
tool_manager_ = std::make_shared<MCPToolManager>(mcp_client_);
if (!tool_manager_->initialize()) { // 内部调用 tools/list 获取工具列表
mcp_client_->disconnect();
mcp_client_.reset();
tool_manager_.reset();
return false;
}
connected_ = true;
return true;
}
连接过程底层:
MCPClient::connect("/path/to/mcp_server", args)
│
├── fork() + exec() // 启动 MCP Server 子进程
│ ↓
│ MCP Server 启动
│ ├── 加载插件目录下的 .so 文件
│ ├── dlopen("calculator.so") → dlsym("CreatePlugin")
│ ├── dlopen("weather.so") → dlsym("CreatePlugin")
│ └── 开始监听 STDIN
│
├── 建立 STDIN/STDOUT 管道(pipe)
│
└── 发送 JSON-RPC: {"method": "initialize", ...}
↓
MCP Server 响应: {"result": {"capabilities": {...}}}
3.3 updateToolCache():发现并缓存工具
cpp
void MCPAgentIntegration::updateToolCache() {
if (!tool_manager_) return;
std::lock_guard<std::mutex> lock(tool_cache_mutex_);
tool_cache_.clear();
// ★ 通过 MCPToolManager 获取工具列表
// 内部调用 JSON-RPC: {"method": "tools/list"}
auto mcp_tools = tool_manager_->getAvailableTools();
for (const auto& mcp_tool : mcp_tools) {
ToolInfo info;
info.name = mcp_tool.name; // "calculator", "get_weather", ...
info.description = mcp_tool.description; // "Evaluates a mathematical expression"
info.input_schema = mcp_tool.input_schema; // JSON Schema 字符串
tool_cache_.push_back(info);
}
// ★ 如果 RAG 已初始化,同步更新向量索引
if (rag_initialized_ && tool_retriever_) {
tool_retriever_->indexTools(tool_cache_);
}
}
3.4 callTool():调用三方工具(带重试)
cpp
ToolCallResult MCPAgentIntegration::callTool(const std::string& tool_name,
const std::string& arguments) {
ToolCallResult result;
auto start_time = std::chrono::steady_clock::now();
// 前置检查
if (!isAvailable()) {
result.success = false;
result.error = "MCP is not available";
return result;
}
if (!hasToolAvailable(tool_name)) {
result.success = false;
result.error = "Tool not found: " + tool_name;
return result;
}
// ★ 带重试的工具调用
int retry_count = 0;
while (retry_count <= config_.max_retry_count) {
try {
// 通过 MCPToolManager 执行工具
// 内部发送 JSON-RPC: {"method": "tools/call", "params": {"name": "calculator", "arguments": {...}}}
MCPResponse response = tool_manager_->executeTool(tool_name, arguments);
auto end_time = std::chrono::steady_clock::now();
result.duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
if (response.is_error) {
result.success = false;
result.error = response.error;
} else {
result.success = true;
result.result = response.result; // ★ 工具执行结果
}
return result;
} catch (const std::exception& e) {
retry_count++;
if (retry_count <= config_.max_retry_count) {
// ★ 线性递增延迟重试
std::this_thread::sleep_for(
std::chrono::milliseconds(config_.retry_delay_ms * retry_count));
} else {
result.success = false;
result.error = "Tool call failed after retries: " + std::string(e.what());
}
}
}
return result;
}
工具调用的 JSON-RPC 通信:
json
// Client → Server (STDIN)
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "calculator", "arguments": {"expression": "123 + 456"}}}
// Server → Client (STDOUT)
{"jsonrpc": "2.0", "id": 1,
"result": {"content": [{"type": "text", "text": "579"}]}}
四、MathAgent:LLM + RAG + MCP 完整集成
MathAgent 是最能体现端到端流程的组件------它将 QwenClient(LLM)、MCPAgentIntegration(MCP 工具)和 RAG 检索三者串联起来。
4.1 MathAgent 成员
cpp
class MathAgent {
private:
std::string agent_id_;
std::string listen_address_;
std::shared_ptr<RedisTaskStore> task_store_; // Redis 历史记录
QwenClient qwen_client_; // ★ 阿里百炼 LLM
RegistryClient registry_client_; // 服务注册
std::unique_ptr<MCPAgentIntegration> mcp_integration_; // ★ MCP 工具集成
public:
MathAgent(/* ... */, const MCPAgentConfig& mcp_config)
: qwen_client_(api_key) // 用 API Key 初始化 LLM
, mcp_integration_(std::make_unique<MCPAgentIntegration>()) {
// ★ 初始化 MCP 集成(连接 MCP Server + 发现工具 + 启用 RAG)
if (!mcp_integration_->initialize(mcp_config)) {
std::cerr << "[MathAgent] MCP 初始化失败,将在无 MCP 模式下运行" << std::endl;
} else if (mcp_integration_->isAvailable()) {
// 输出可用工具
auto tools = mcp_integration_->getToolNames();
std::cout << "[MathAgent] MCP 已启用,可用工具: ";
for (const auto& tool : tools) {
std::cout << tool << " ";
}
std::cout << std::endl;
}
}
};
4.2 solve_math():核心解题流程(LLM + MCP 协同)
cpp
std::string solve_math(const std::string& question, const std::string& context_id) {
// ① 获取 Redis 中的历史对话
auto history = task_store_->get_history(context_id, 5);
std::string history_text;
for (const auto& msg : history) {
/* 拼接历史消息 */
}
// ② ★ 尝试使用 MCP 工具进行计算
std::string tool_result;
if (mcp_integration_ && mcp_integration_->isAvailable()) {
tool_result = tryMCPCalculation(question);
}
// ③ 构造系统提示词
std::string system_prompt =
"你是一个专业的数学助手。请解答用户的数学问题,给出详细的解题步骤。"
"如果是计算题,请给出准确的计算结果。";
// ④ ★ 如果 MCP 工具有计算结果,注入到 prompt 中
if (!tool_result.empty()) {
system_prompt += "\n\n工具计算结果参考:\n" + tool_result;
}
// ⑤ ★ 调用通义千问 LLM 生成最终回答
return qwen_client_.chat(
system_prompt + "\n\n历史对话:\n" + history_text,
question
);
}
这里的设计理念 :MCP 工具提供精确的计算结果 (如 "579"),LLM 负责组织自然语言回答(如 "123 + 456 = 579。计算过程是...")。工具结果作为"参考资料"注入到 system prompt 中,LLM 基于此生成更准确、更有结构的回答。
4.3 tryMCPCalculation():RAG 智能检索 + 工具调用
cpp
std::string tryMCPCalculation(const std::string& question) {
if (!mcp_integration_ || !mcp_integration_->isAvailable()) {
return "";
}
// ═══════════════════════════════════════════
// 第 ① 步: 使用 RAG 智能检索相关工具
// ═══════════════════════════════════════════
std::vector<ToolInfo> relevant_tools;
if (mcp_integration_->isRAGEnabled()) {
std::cout << "[MathAgent] 使用 RAG 检索相关工具..." << std::endl;
// ★ 将用户问题转为向量,在向量索引中搜索最相似的 5 个工具
relevant_tools = mcp_integration_->getRelevantTools(question, 5);
std::cout << "[MathAgent] RAG 检索到 " << relevant_tools.size() << " 个相关工具: ";
for (const auto& tool : relevant_tools) {
std::cout << tool.name << " ";
}
std::cout << std::endl;
} else {
// RAG 未启用 → 回退到手动检查特定工具
std::cout << "[MathAgent] RAG 未启用,使用默认工具检查" << std::endl;
if (mcp_integration_->hasToolAvailable("calculator")) {
ToolInfo calc_tool;
calc_tool.name = "calculator";
relevant_tools.push_back(calc_tool);
}
}
// ═══════════════════════════════════════════
// 第 ② 步: 从检索结果中找到 calculator 并调用
// ═══════════════════════════════════════════
for (const auto& tool : relevant_tools) {
if (tool.name == "calculator" || tool.name == "calculate" || tool.name == "math") {
json args;
args["expression"] = question;
std::cout << "[MathAgent] 调用 MCP 工具: " << tool.name << std::endl;
// ★ 通过 MCP 协议调用工具
auto result = mcp_integration_->callTool(tool.name, args.dump());
if (result.success) {
std::cout << "[MathAgent] MCP 工具返回: " << result.result << std::endl;
return result.result; // 返回计算结果(如 "579")
} else {
std::cerr << "[MathAgent] MCP 工具调用失败: " << result.error << std::endl;
}
}
}
// ═══════════════════════════════════════════
// 第 ③ 步: 降级 --- 如果 RAG 没检索到 calculator,直接尝试
// ═══════════════════════════════════════════
if (mcp_integration_->hasToolAvailable("calculator")) {
json args;
args["expression"] = question;
std::cout << "[MathAgent] 回退使用 calculator 工具" << std::endl;
auto result = mcp_integration_->callTool("calculator", args.dump());
if (result.success) {
return result.result;
}
}
return ""; // 所有尝试都失败
}
tryMCPCalculation 的三层容错:
- RAG 检索 → 从语义相似度最高的工具中找 calculator
- RAG 降级 → RAG 未启用时,手动检查 calculator 是否存在
- 直接回退 → RAG 检索结果中没有 calculator,直接按名字尝试
五、AIOrchestrator:意图识别 + 工具增强
Orchestrator 是调度层,负责接收用户请求、识别意图、路由到合适的 Agent。
5.1 意图识别:调用 LLM 分类
cpp
std::string analyze_intent(const std::string& text) {
// ★ 用 LLM 做意图分类
std::string prompt = "判断以下用户输入属于哪个类别,只回答类别名称:\n"
"- math: 数学计算、方程求解\n"
"- code: 编程、代码相关\n"
"- general: 其他对话\n\n"
"用户输入: " + text;
std::string result = qwen_client_.chat("", prompt);
// 解析 LLM 返回的类别
if (result.find("math") != std::string::npos) return "math";
if (result.find("code") != std::string::npos) return "code";
return "general";
}
5.2 处理请求:意图 → 路由 → 响应
cpp
std::string handle_request(const std::string& body) {
// 解析 A2A JSON-RPC 请求
auto request = JsonRpcRequest::from_json(body);
if (request.method() == "message/send") {
// 提取用户文本
std::string user_text = /* 从 AgentMessage 中提取 */;
// ★ 识别意图
std::string intent = analyze_intent(user_text);
std::string response_text;
if (intent == "math") {
// ★ 路由到 MathAgent(通过服务注册中心动态发现)
response_text = call_math_agent(user_text, context_id);
} else if (intent == "code") {
response_text = call_code_agent(user_text, context_id);
} else {
// 通用对话:直接调 LLM + 尝试 MCP 工具增强
response_text = handle_general_query(user_text, context_id);
}
// 返回 A2A JSON-RPC 响应
return JsonRpcResponse::create_success(request.id(), response_msg.to_json()).to_json();
}
}
5.3 tryMCPTools():通用查询的 MCP 工具增强
cpp
std::string tryMCPTools(const std::string& query) {
if (!mcp_integration_ || !mcp_integration_->isAvailable()) {
return "";
}
std::string result;
auto tools = mcp_integration_->getAvailableTools();
// 根据工具名称和查询内容做简单匹配
for (const auto& tool : tools) {
bool should_use = false;
// 搜索类工具 → 总是尝试
if (tool.name.find("search") != std::string::npos ||
tool.name.find("query") != std::string::npos) {
should_use = true;
}
// 时间类工具 → 查询包含"时间"关键词才使用
else if (tool.name.find("time") != std::string::npos) {
if (query.find("时间") != std::string::npos ||
query.find("time") != std::string::npos) {
should_use = true;
}
}
if (should_use) {
json args;
args["query"] = query;
// ★ 调用 MCP 工具获取辅助信息
auto tool_result = mcp_integration_->callTool(tool.name, args.dump());
if (tool_result.success) {
result += "[" + tool.name + "]: " + tool_result.result + "\n";
}
}
}
return result; // 返回工具辅助信息,注入到 LLM prompt 中
}
5.4 handle_general_query():LLM + MCP 工具协同回答
cpp
std::string handle_general_query(const std::string& query, const std::string& context_id) {
// 获取历史对话
auto history = task_store_->get_history(context_id, 5);
std::string history_text = /* 拼接历史 */;
// ★ 尝试 MCP 工具获取辅助信息
std::string tool_context = tryMCPTools(query);
if (!tool_context.empty()) {
// 工具信息注入到 prompt
history_text += "\n工具辅助信息:\n" + tool_context;
}
// ★ 调用 LLM 生成回答(带工具辅助)
return qwen_client_.chat(history_text, query);
}
六、gRPC AI Demo:另一种集成方式
这是通过 gRPC 协议 暴露 AI 查询服务的示例,直接调用通义千问 API。
6.1 DirectAIQueryServiceImpl
cpp
class DirectAIQueryServiceImpl final : public agent_communication::AIQueryService::Service {
public:
DirectAIQueryServiceImpl(const std::string& api_key, const std::string& model = "qwen-plus")
: api_key_(api_key), model_(model) {
curl_global_init(CURL_GLOBAL_DEFAULT);
}
// ★ 同步查询:接收 gRPC 请求,调用通义千问,返回 gRPC 响应
Status Query(ServerContext* context,
const agent_communication::AIQueryRequest* request,
agent_communication::AIQueryResponse* response) override {
// 调用通义千问 API(使用 OpenAI 兼容接口)
std::string answer, error;
bool success = callQwenAPI(request->question(), answer, error);
response->set_request_id(request->request_id());
response->set_processing_time_ms(duration.count());
if (success) {
response->set_answer(answer);
response->set_agent_name("DirectAIService");
return Status::OK;
} else {
return Status(grpc::StatusCode::INTERNAL, error);
}
}
// ★ 流式查询:分块返回 AI 回答
Status QueryStream(ServerContext* context,
const agent_communication::AIQueryRequest* request,
ServerWriter<agent_communication::AIStreamEvent>* writer) override {
std::string answer, error;
bool success = callQwenAPI(request->question(), answer, error);
if (success) {
// 将回答分块流式发送(模拟打字效果)
size_t chunk_size = 50;
for (size_t i = 0; i < answer.length(); i += chunk_size) {
agent_communication::AIStreamEvent chunk_event;
chunk_event.set_event_type("partial");
chunk_event.set_content(answer.substr(i, chunk_size));
writer->Write(chunk_event);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// 发送完成事件
agent_communication::AIStreamEvent complete_event;
complete_event.set_event_type("complete");
complete_event.set_content(answer);
writer->Write(complete_event);
}
return Status::OK;
}
};
6.2 callQwenAPI():OpenAI 兼容模式
gRPC Demo 使用了 DashScope 的 OpenAI 兼容接口(与 QwenClient 用的原生接口不同):
cpp
bool callQwenAPI(const std::string& question, std::string& answer, std::string& error) {
// ★ 使用 jsoncpp (Json::Value) 而非 nlohmann/json
Json::Value request_json;
request_json["model"] = model_;
Json::Value message;
message["role"] = "user";
message["content"] = question;
request_json["messages"].append(message);
// ★ OpenAI 兼容 API 端点
std::string url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
// ... curl 请求 ...
// ★ OpenAI 格式响应解析
// 路径: response["choices"][0]["message"]["content"]
answer = response_json["choices"][0]["message"]["content"].asString();
return true;
}
两种 API 对比:
| 特性 | 原生 API (QwenClient) | OpenAI 兼容 API (gRPC Demo) |
|---|---|---|
| URL | /api/v1/services/aigc/text-generation/generation |
/compatible-mode/v1/chat/completions |
| 请求格式 | {"model":"...", "input":{"messages":[...]}} |
{"model":"...", "messages":[...]} |
| 响应格式 | output.choices[0].message.content |
choices[0].message.content |
| JSON 库 | nlohmann/json | jsoncpp (Json::Value) |
七、RPC + MCP 集成测试
这是一个独立的集成测试程序,验证 RPC 服务器与 MCP 工具的端到端工作。
7.1 测试流程
cpp
int main() {
// ① 创建 RPC 服务器
RpcServer rpc_server;
// ② 设置 MCP Server 路径和插件目录
rpc_server.setMCPServerPath("/root/agent-communication/mcp_server_integrated/build/mcp_server");
rpc_server.setMCPServerArgs({
"-n", "rpc-integrated-server",
"-l", "/tmp/mcp_logs",
"-p", "/root/agent-communication/mcp_server_integrated/plugins"
});
// ③ 初始化并启动 RPC 服务器(内部会启动 MCP Server 子进程)
rpc_server.initialize(config);
rpc_server.start();
// ④ 获取可用的 AI 工具
auto service = rpc_server.getService();
auto available_tools = service->getAvailableAITools();
// 输出: calculator, get_weather, sleep, ...
// ⑤ ★ 测试 sleep 工具
auto response = service->callAITool("sleep", R"({"milliseconds": 2000})");
// 结果: {"slept_ms": 2000, "message": "Slept for 2000 milliseconds"}
// ⑥ ★ 测试 weather 工具
auto response = service->callAITool("get_weather", R"({
"city": "北京",
"latitude": "39.9042",
"longitude": "116.4074"
})");
// 结果: {"city": "北京", "temperature": "...", "weather": "..."}
rpc_server.wait(); // 保持运行,等待 gRPC 客户端连接
}
八、系统启动脚本:一键部署
bash
#!/bin/bash
# AI Agent 系统启动脚本
# 启动顺序: Registry → Math Agent → Orchestrator
# 配置
REGISTRY_PORT=8500
ORCHESTRATOR_PORT=5000
MATH_AGENT_PORT=5001
API_KEY="${QWEN_API_KEY:-sk-your-api-key}" # 阿里百炼 API Key
DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-}" # RAG Embedding API Key
ENABLE_MCP="${ENABLE_MCP:-false}" # 是否启用 MCP
ENABLE_RAG="${ENABLE_RAG:-false}" # 是否启用 RAG 智能检索
# MCP 参数
if [ "$ENABLE_MCP" == "true" ]; then
MCP_ARGS="--enable-mcp --mcp-server $MCP_SERVER_PATH \
--mcp-args -l,$MCP_LOGS_PATH,-p,$MCP_PLUGINS_PATH"
fi
# RAG 参数
if [ "$ENABLE_RAG" == "true" ] && [ -n "$DASHSCOPE_API_KEY" ]; then
RAG_ARGS="--enable-rag --rag-top-k $RAG_TOP_K --rag-threshold $RAG_THRESHOLD"
fi
# 1. 启动 Registry Server(服务注册中心)
"$BIN_DIR/ai_registry_server" $REGISTRY_PORT &
# 2. 启动 Math Agent(MCP + RAG + LLM)
"$BIN_DIR/ai_math_agent" math-1 $MATH_AGENT_PORT \
http://localhost:$REGISTRY_PORT $API_KEY \
$MCP_ARGS $RAG_ARGS &
# 3. 启动 Orchestrator(调度器)
"$BIN_DIR/ai_orchestrator" orch-1 $ORCHESTRATOR_PORT \
http://localhost:$REGISTRY_PORT $API_KEY \
$MCP_ARGS $RAG_ARGS &
启动后的系统架构:
用户
│
▼
┌────────────────┐
│ Orchestrator │ :5000
│ (意图识别+路由) │
└───────┬────────┘
│
┌────────┴────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Math Agent │ │ (其他 Agent) │
│ :5001 │ │ │
│ │ └──────────────┘
│ QwenClient │
│ + MCP + RAG │
└───────┬──────┘
│ STDIO(JSON-RPC)
▼
┌──────────────┐
│ MCP Server │
│ ┌─────────┐ │
│ │calc.so │ │ ← dlopen 动态加载
│ │weather │ │
│ │sleep │ │
│ └─────────┘ │
└──────────────┘
九、完整端到端调用链:用户问 "计算 123 + 456"
步骤 1: 用户发送 A2A 请求到 Orchestrator (:5000)
══════════════════════════════════════════════════
POST http://localhost:5000/
{
"jsonrpc": "2.0", "id": "1", "method": "message/send",
"params": {"message": {"role": "user", "parts": [{"kind": "text", "text": "计算 123 + 456"}]}}
}
步骤 2: Orchestrator 调用 LLM 识别意图
══════════════════════════════════════════════════
analyze_intent("计算 123 + 456")
→ qwen_client_.chat("", "判断...类别...: 计算 123 + 456")
→ DashScope API 返回 "math"
→ intent = "math"
步骤 3: Orchestrator 路由到 Math Agent (:5001)
══════════════════════════════════════════════════
call_math_agent("计算 123 + 456", context_id)
→ 从 Registry 查找 tag="math" 的 Agent → http://localhost:5001
→ POST http://localhost:5001/ { "method": "message/send", ... }
步骤 4: Math Agent 使用 RAG 检索相关工具
══════════════════════════════════════════════════
tryMCPCalculation("计算 123 + 456")
│
├── mcp_integration_->isRAGEnabled() → true
│
└── mcp_integration_->getRelevantTools("计算 123 + 456", 5)
│
├── getEmbedding("计算 123 + 456")
│ └── curl POST DashScope Embedding API → 1536 维向量
│
└── index_->search(query_vec, 5, 0.3)
├── cos(query_vec, "calculator" 向量) = 0.87 ← 最高
├── cos(query_vec, "add" 向量) = 0.82
├── cos(query_vec, "get_weather" 向量) = 0.12
└── 结果: [calculator(0.87), add(0.82)]
[MathAgent] RAG 检索到 2 个相关工具: calculator add
步骤 5: Math Agent 通过 MCP 协议调用 calculator 工具
══════════════════════════════════════════════════
mcp_integration_->callTool("calculator", '{"expression":"123 + 456"}')
│
├── tool_manager_->executeTool("calculator", args)
│ │
│ └── JSON-RPC via STDIO:
│ → {"method":"tools/call","params":{"name":"calculator","arguments":{"expression":"123+456"}}}
│ ← {"result":{"content":[{"type":"text","text":"579"}]}}
│
└── result = "579"
[MathAgent] 调用 MCP 工具: calculator
[MathAgent] MCP 工具返回: 579
步骤 6: Math Agent 将工具结果 + 用户问题发给 LLM
══════════════════════════════════════════════════
system_prompt = "你是数学助手...\n\n工具计算结果参考:\n579"
qwen_client_.chat(system_prompt, "计算 123 + 456")
→ DashScope Chat API
→ "123 + 456 = 579。\n\n这是一个简单的加法运算..."
步骤 7: 响应逐层返回
══════════════════════════════════════════════════
Math Agent → Orchestrator → 用户
"123 + 456 = 579。这是一个简单的加法运算..."
十、MCPToolManager 和 MCPClient 的角色
10.1 MCPToolManager
cpp
class MCPToolManager {
public:
MCPToolManager(std::shared_ptr<IMCPClient> mcp_client);
bool initialize(); // 调用 tools/list 获取初始工具列表
void shutdown();
// 工具查询
std::vector<MCPTool> getAvailableTools() const;
bool isToolAvailable(const std::string& tool_name) const;
// ★ 工具执行:发送 tools/call JSON-RPC 请求
MCPResponse executeTool(const std::string& tool_name, const std::string& arguments);
// 异步执行
void executeToolAsync(const std::string& tool_name,
const std::string& arguments,
std::function<void(const MCPResponse&)> callback);
// 参数验证
bool validateToolArguments(const std::string& tool_name, const std::string& arguments) const;
private:
std::shared_ptr<IMCPClient> mcp_client_;
std::vector<MCPTool> available_tools_;
std::map<std::string, MCPTool> tool_map_; // name → MCPTool(O(1) 查找)
mutable std::mutex tools_mutex_;
};
10.2 组件调用链
MCPAgentIntegration.callTool("calculator", args)
│
▼
MCPToolManager.executeTool("calculator", args)
│
├── 验证工具存在: tool_map_.find("calculator")
│
├── 构造 JSON-RPC 请求
│ {"jsonrpc":"2.0","method":"tools/call",
│ "params":{"name":"calculator","arguments":{...}}}
│
└── 发送给 MCPClient
│
▼
MCPClient.sendRequest(json_rpc)
│
├── write(STDIN pipe) → MCP Server 子进程
│
└── read(STDOUT pipe) ← MCP Server 子进程
│
▼
MCP Server 内部:
├── 路由到 calculator 插件
├── PluginAPI::HandleToolCall("calculator", args)
├── 执行计算: 123 + 456 = 579
└── 返回: {"content":[{"type":"text","text":"579"}]}
十一、GTest 单元测试
测试分为两组:
11.1 Task 20.1: 初始化和生命周期测试
cpp
// 默认构建状态
TEST_F(MCPAgentIntegrationTest, DefaultConstruction) {
EXPECT_FALSE(integration_->isInitialized());
EXPECT_FALSE(integration_->isAvailable());
}
// MCP 禁用时的初始化
TEST_F(MCPAgentIntegrationTest, InitializeWithMCPDisabled) {
MCPAgentConfig config;
config.enable_mcp = false;
bool result = integration_->initialize(config);
EXPECT_TRUE(result); // 初始化成功
EXPECT_TRUE(integration_->isInitialized());
EXPECT_FALSE(integration_->isAvailable()); // 但 MCP 不可用
EXPECT_EQ(integration_->getStatusDescription(), "MCP disabled");
}
// 空路径 → 降级模式
TEST_F(MCPAgentIntegrationTest, InitializeWithEmptyServerPath) {
MCPAgentConfig config;
config.enable_mcp = true;
config.mcp_server_path = "";
bool result = integration_->initialize(config);
EXPECT_TRUE(result); // 降级模式仍然成功
EXPECT_TRUE(integration_->isInitialized());
EXPECT_FALSE(integration_->isAvailable());
}
// 重复初始化
TEST_F(MCPAgentIntegrationTest, DoubleInitialization) {
MCPAgentConfig config;
config.enable_mcp = false;
bool result1 = integration_->initialize(config);
bool result2 = integration_->initialize(config);
EXPECT_TRUE(result1);
EXPECT_TRUE(result2); // 幂等性:第二次直接返回 true
}
11.2 Task 20.2: 错误处理测试
cpp
// MCP 不可用时调用工具
TEST_F(MCPAgentIntegrationTest, ToolCallWhenMCPNotAvailable) {
MCPAgentConfig config;
config.enable_mcp = false;
integration_->initialize(config);
auto result = integration_->callTool("calculator", R"({"a": 1, "b": 2})");
EXPECT_FALSE(result.success);
EXPECT_TRUE(result.error.find("not available") != std::string::npos);
}
// 未初始化时调用工具
TEST_F(MCPAgentIntegrationTest, ToolCallWhenNotInitialized) {
auto result = integration_->callTool("calculator", R"({"a": 1, "b": 2})");
EXPECT_FALSE(result.success);
EXPECT_FALSE(result.error.empty());
}
// callToolSimple 错误格式
TEST_F(MCPAgentIntegrationTest, CallToolSimpleReturnsErrorPrefix) {
MCPAgentConfig config;
config.enable_mcp = false;
integration_->initialize(config);
std::string result = integration_->callToolSimple("calculator", R"({})");
EXPECT_TRUE(result.find("[ERROR]") == 0); // 返回带 [ERROR] 前缀
}
11.3 属性测试(RapidCheck)
cpp
// 属性:配置值在 initialize 后保持不变
RC_GTEST_PROP(MCPConfigProperties, ConfigPreservesValues, ()) {
auto timeout = *rc::gen::inRange(100, 60000);
auto retry_count = *rc::gen::inRange(0, 10);
auto retry_delay = *rc::gen::inRange(100, 5000);
MCPAgentConfig config;
config.enable_mcp = false;
config.tool_call_timeout_ms = timeout;
config.max_retry_count = retry_count;
config.retry_delay_ms = retry_delay;
MCPAgentIntegration integration;
integration.initialize(config);
// ★ 属性:配置值在初始化后精确保持
const auto& stored = integration.getConfig();
RC_ASSERT(stored.tool_call_timeout_ms == timeout);
RC_ASSERT(stored.max_retry_count == retry_count);
RC_ASSERT(stored.retry_delay_ms == retry_delay);
}
十二、面试话术
面试官:你怎么进行功能测试的?
我建了一个端到端的集成验证环境。启动脚本会依次启动 Registry Server(服务注册中心)、Math Agent(带 MCP 和 RAG)和 Orchestrator(调度器)。然后用户发送一个数学问题,比如"计算 123+456",Orchestrator 先用通义千问做意图识别,判断为 math 类别,路由到 Math Agent。Math Agent 用 RAG 从向量索引里检索到 calculator 工具,通过 MCP 协议的
tools/call方法调用它,得到精确结果 579,然后把这个结果注入到通义千问的 system prompt 里,LLM 基于此生成自然语言回答返回给用户。整个链路验证了 LLM 调用、MCP 工具发现与调用、RAG 智能检索、服务注册发现这些核心功能。
面试官:QwenClient 是怎么和 MCP 工具配合的?它俩的配合是"先工具后 LLM"的模式。MathAgent 先通过 MCPAgentIntegration 调用 calculator 工具获取精确的计算结果,然后把这个结果作为"参考资料"追加到 LLM 的 system prompt 里,再调用通义千问。这样 LLM 不用自己算(大模型算数不可靠),而是基于工具提供的精确结果来组织自然语言回答,兼顾了计算精确性 和回答自然性。
面试官:MCP 协议在这个流程中具体做了什么?MCP 协议主要做了三件事。第一是工具发现 :MCPClient 启动 MCP Server 子进程后,通过 STDIO 管道发送
tools/listJSON-RPC 请求,Server 返回所有注册的工具列表(名字、描述、参数 Schema),MCPToolManager 缓存下来。第二是工具调用 :Agent 需要用工具时,通过tools/call发送请求,Server 路由到对应的插件执行并返回结果。第三是动态加载 :MCP Server 用dlopen动态加载插件 .so 文件,插件实现统一的PluginAPI接口,新工具只要编译成 .so 放到插件目录就能被发现和调用,不需要改 Server 代码。
面试官:这个端到端流程中有哪些容错机制?至少有 5 层容错。第一,MCP Server 连接失败时进入降级模式 ,Agent 仍然可以只用 LLM 回答(没有工具辅助)。第二,
callTool有重试机制 (线性递增延迟重试max_retry_count次)。第三,RAG 检索失败时自动回退 到返回所有工具或手动匹配工具名。第四,LLM API 调用失败有异常捕获和错误提示。第五,Orchestrator 调用子 Agent 失败会降级到用通用模型直接回答。
整体集成学习文档
整体集成:对用户只提供一个 Client 接口,Server 部分就是一个"超级 AI 智能体"
一、整体集成概述------"超级 AI 智能体"幻象
1.1 核心目标
这个项目在底层由多个独立 Agent + 多种 MCP 工具 + LLM 模型 协同运作,但对用户而言,只需一个 RpcClient 对象,调一个 aiQuery() 方法,就像在跟一个"超级 AI 智能体"对话。用户完全不知道背后有 Orchestrator 做意图路由、有 MathAgent 处理数学、有 MCP 工具增强能力、有 Redis 存储上下文。
1.2 端到端架构
用户
|
| 简单的 C++ API 调用
v
RpcClient (client/)
|
| gRPC / Protobuf
v
RpcServer (server/)
|
| 内部持有 AIQueryServiceImpl
| 通过 A2AAdapter 桥接协议
v
A2AAdapter (a2a_adapter/)
|
| JSON-RPC over HTTP
v
Orchestrator (examples/ai_orchestrator/)
|
|--- 意图识别 (LLM)
|--- 路由到专业 Agent (通过 Registry 服务发现)
|--- MCP 工具调用 (通过 MCPAgentIntegration)
|
v
Math Agent / Code Agent / MCP Tools / LLM (通义千问)
关键设计思想 :每一层只知道自己的上层和下层,用户只与 RpcClient 交互,所有复杂性被逐层封装。
二、统一客户端入口------RpcClient
2.1 RpcClient 的"门面"设计
文件:<client/include/agent_rpc/client/rpc_client.h>
RpcClient 是整个系统暴露给用户的唯一入口 。它采用 组合模式 ,内部持有一个 AIQueryClient,但对外只暴露统一的 AI 查询接口:
cpp
class RpcClient {
public:
// 连接管理------用户只需知道服务器地址
bool initialize(const common::RpcConfig& config);
bool connect(const std::string& server_address);
void disconnect();
// ★ 核心 AI 查询接口------用户唯一需要的方法
agent_communication::AIQueryResponse aiQuery(
const std::string& question,
const std::string& context_id = "",
int timeout_seconds = 30);
// ★ 流式查询------实时获取 AI 回答
bool aiQueryStream(
const std::string& question,
StreamEventCallback callback,
const std::string& context_id = "",
int timeout_seconds = 60);
private:
// 内部组合 AIQueryClient,用户不可见
std::unique_ptr<AIQueryClient> ai_query_client_;
};
面试话术 :RpcClient 采用了门面模式(Facade Pattern),将 gRPC 连接管理、AI 查询、流式通信等复杂功能统一封装。用户只需 connect() + aiQuery() 两步操作,就能与整个多 Agent 系统交互。
2.2 连接时自动组装内部组件
文件:<client/src/rpc_client.cpp>
构造函数中,RpcClient 自动创建 AIQueryClient:
cpp
RpcClient::RpcClient()
: heartbeat_running_(false)
, connection_retry_count_(0)
, ai_query_client_(std::make_unique<AIQueryClient>()) {
}
当用户调用 connect() 时,RpcClient 同时将 AIQueryClient 连接到同一服务器:
cpp
bool RpcClient::connect(const std::string& server_address) {
server_address_ = server_address;
try {
setupChannel();
connected_ = true;
// ★ 关键:自动将 AIQueryClient 连接到同一服务器
if (ai_query_client_) {
if (!ai_query_client_->connect(server_address)) {
LOG_WARN("Failed to connect AIQueryClient, AI queries will not be available");
}
}
LOG_INFO("Connected to RPC server: " + server_address);
return true;
} catch (...) { ... }
}
用户调用 aiQuery() 时,实际委托给内部 AIQueryClient:
cpp
agent_communication::AIQueryResponse RpcClient::aiQuery(
const std::string& question,
const std::string& context_id,
int timeout_seconds) {
if (!ai_query_client_ || !ai_query_client_->isConnected()) {
// 返回错误响应
...
}
return ai_query_client_->query(question, context_id, timeout_seconds);
}
设计分析 :这是组合 + 委托 的经典用法。RpcClient 不继承 AIQueryClient,而是持有它的 unique_ptr,遵循"组合优于继承"原则。好处是:
AIQueryClient可以独立测试和复用RpcClient可以随时替换底层 AI 查询实现getAIQueryClient()方法也允许高级用户直接操作
2.3 AIQueryClient------与 gRPC 服务通信
文件:<client/include/agent_rpc/client/ai_query_client.h>
AIQueryClient 负责与 AIQueryService gRPC 服务通信:
cpp
class AIQueryClient {
public:
bool connect(const std::string& server_address);
// 同步查询
agent_communication::AIQueryResponse query(
const std::string& question,
const std::string& context_id = "",
int timeout_seconds = 30);
// 流式查询
bool queryStream(
const std::string& question,
StreamEventCallback callback,
const std::string& context_id = "",
int timeout_seconds = 60);
// 查询状态
agent_communication::QueryStatusResponse getQueryStatus(
const std::string& task_id,
const std::string& context_id = "");
private:
std::unique_ptr<agent_communication::AIQueryService::Stub> stub_;
std::shared_ptr<grpc::Channel> channel_;
};
文件:<client/src/ai_query_client.cpp>
同步查询的核心实现:
cpp
agent_communication::AIQueryResponse AIQueryClient::query(
const agent_communication::AIQueryRequest& request) {
agent_communication::AIQueryResponse response;
grpc::ClientContext context;
int timeout = request.timeout_seconds() > 0 ? request.timeout_seconds() : 30;
auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(timeout);
context.set_deadline(deadline);
// ★ 一行 gRPC 调用,背后是整个多 Agent 系统
grpc::Status status = stub_->Query(&context, request, &response);
if (status.ok()) {
if (response.status().code() == 0) {
LOG_INFO("AI query completed");
} else {
LOG_ERROR("AI query failed: " + response.status().message());
}
} else {
LOG_ERROR("gRPC failed: " + status.error_message());
}
return response;
}
流式查询实现------使用 ClientReader 接收 Server-Streaming RPC:
cpp
bool AIQueryClient::queryStream(
const agent_communication::AIQueryRequest& request,
StreamEventCallback callback) {
grpc::ClientContext context;
// ★ 发起 Server-Streaming RPC
std::unique_ptr<grpc::ClientReader<agent_communication::AIStreamEvent>> reader(
stub_->QueryStream(&context, request));
agent_communication::AIStreamEvent event;
int event_count = 0;
// 循环读取流式事件
while (reader->Read(&event)) {
event_count++;
callback(event);
if (event.event_type() == "complete" || event.event_type() == "error") {
break;
}
}
grpc::Status status = reader->Finish();
return status.ok();
}
面试话术 :客户端的 queryStream 使用了 gRPC 的 Server-Streaming RPC 模式。客户端发送一个请求,服务端通过流返回多个事件(partial/status/complete/error),客户端通过回调函数实时处理每个事件。这实现了类似 ChatGPT 的"打字机效果"。
三、Protobuf 服务定义------AIQueryService
文件:<proto/ai_query.proto>
这是连接客户端和服务端的"契约":
protobuf
service AIQueryService {
// 同步查询
rpc Query(AIQueryRequest) returns (AIQueryResponse);
// 流式查询 (Server-Streaming)
rpc QueryStream(AIQueryRequest) returns (stream AIStreamEvent);
// 获取查询状态
rpc GetQueryStatus(QueryStatusRequest) returns (QueryStatusResponse);
}
请求消息设计------支持多轮对话和 Agent 偏好:
protobuf
message AIQueryRequest {
string request_id = 1; // 请求唯一ID
string question = 2; // 用户问题
string context_id = 3; // 上下文ID (用于多轮对话)
int32 history_length = 4; // 历史消息长度限制
int32 timeout_seconds = 5; // 超时时间
map<string, string> metadata = 6; // 元数据
AgentPreference preference = 7; // Agent偏好设置
}
响应消息------携带完整的处理结果:
protobuf
message AIQueryResponse {
string request_id = 1; // 请求ID
common.Status status = 2; // 状态
string answer = 3; // ★ AI 回答
string agent_id = 4; // 处理的 Agent ID
string agent_name = 5; // 处理的 Agent 名称
string task_id = 6; // A2A 任务ID
string context_id = 7; // 上下文ID
repeated Artifact artifacts = 8; // 产物列表
int64 processing_time_ms = 9; // 处理时间
}
设计分析 :响应中携带了 agent_id 和 agent_name,让用户可以知道是哪个 Agent 回答了问题,但这对用户是透明可选的。context_id 支持多轮对话追踪。artifacts 支持返回文件等附件数据。
四、统一 gRPC 服务端------RpcServer
4.1 三合一服务注册
文件:<server/include/agent_rpc/server/rpc_server.h>
RpcServer 将三个独立服务组装到同一个 gRPC 进程中:
cpp
class RpcServer {
public:
bool initialize(const common::RpcConfig& config);
bool start();
void stop();
// 三个服务,一个进程
std::shared_ptr<AgentCommunicationServiceImpl> getService(); // Agent 通信
std::shared_ptr<HealthServiceImpl> getHealthService(); // 健康检查
std::shared_ptr<AIQueryServiceImpl> getAIQueryService(); // ★ AI 查询
// 配置后端
void setA2AConfig(const a2a_adapter::A2AConfig& config);
void setMCPServerPath(const std::string& path);
private:
std::unique_ptr<grpc::Server> server_;
std::shared_ptr<AgentCommunicationServiceImpl> service_impl_;
std::shared_ptr<HealthServiceImpl> health_service_impl_;
std::shared_ptr<AIQueryServiceImpl> ai_query_service_impl_;
a2a_adapter::A2AConfig a2a_config_;
};
4.2 初始化流程------所有组件一次性装配
文件:<server/src/rpc_server.cpp>
cpp
bool RpcServer::initialize(const common::RpcConfig& config) {
config_ = config;
address_ = config.server_address;
// ★ 一次性创建三个服务实现
service_impl_ = std::make_shared<AgentCommunicationServiceImpl>();
health_service_impl_ = std::make_shared<HealthServiceImpl>();
ai_query_service_impl_ = std::make_shared<AIQueryServiceImpl>();
// 初始化序列化器
common::MessageSerializer::getInstance().initialize(
common::SerializerFactory::PROTOBUF_BINARY);
// ★ 初始化 AI 查询服务(内部创建 A2AAdapter)
if (!ai_query_service_impl_->initialize(config_, a2a_config_)) {
LOG_WARN("Failed to initialize AI Query Service, continuing without it");
}
setupServer();
return true;
}
setupServer() 将服务注册到 gRPC:
cpp
void RpcServer::setupServer() {
grpc::ServerBuilder builder;
builder.AddListeningPort(address_, grpc::InsecureServerCredentials());
builder.SetMaxReceiveMessageSize(config_.max_receive_message_size);
builder.SetMaxSendMessageSize(config_.max_message_size);
// ★ 注册 AI 查询服务到 gRPC
if (ai_query_service_impl_ && ai_query_service_impl_->isAvailable()) {
builder.RegisterService(ai_query_service_impl_.get());
}
// 启用健康检查和服务反射
grpc::EnableDefaultHealthCheckService(true);
grpc::reflection::InitProtoReflectionServerBuilderPlugin();
// 配置 Keepalive 参数
builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_TIME_MS, 30000);
builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, 5000);
builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, true);
server_ = builder.BuildAndStart();
}
面试话术 :RpcServer 使用 ServerBuilder 模式一次性完成多服务注册,对用户来说只暴露一个端口(默认 50051),但内部可以提供多个 gRPC 服务。这是 gRPC 的多服务复用能力------一个 grpc::Server 可以注册多个 Service。
4.3 服务端 main 函数------启动即完成
文件:<server/src/main.cpp>
cpp
int main(int argc, char* argv[]) {
// 配置 RPC Server
RpcConfig config;
config.server_address = "0.0.0.0:" + port;
// 配置 A2A 适配器------指向 Orchestrator
a2a_adapter::A2AConfig a2a_config;
a2a_config.orchestrator_url = orchestrator_url; // 默认 http://localhost:5000
// 创建并初始化
RpcServer server;
server.setA2AConfig(a2a_config);
server.initialize(config);
server.start();
// 主循环
while (g_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
server.stop();
}
注意启动架构注释(来自 main.cpp 头部注释):
rpc_client ──gRPC──> rpc_server ──A2A/HTTP──> Orchestrator ──> Agents
这行注释精准地概括了整个系统的三层调用链。
五、协议桥接核心------A2AAdapter 适配器层
5.1 适配器模式的整体设计
文件:<a2a_adapter/include/agent_rpc/a2a_adapter/a2a_adapter.h>
A2AAdapter 是整个系统最关键的"胶水层",它将 gRPC 协议翻译为 A2A 协议:
cpp
class A2AAdapter {
public:
bool initialize(const A2AConfig& config);
// ★ 同步查询:gRPC Request → A2A Message → A2A Response → gRPC Response
bool processQuery(
const agent_communication::AIQueryRequest& request,
agent_communication::AIQueryResponse* response);
// ★ 异步查询
void processQueryAsync(
const agent_communication::AIQueryRequest& request,
std::function<void(const agent_communication::AIQueryResponse&)> callback);
// ★ 流式查询:gRPC Request → A2A Streaming → gRPC Stream Events
void processQueryStreaming(
const agent_communication::AIQueryRequest& request,
std::function<void(const agent_communication::AIStreamEvent&)> callback);
private:
std::unique_ptr<a2a::A2AClient> a2a_client_; // A2A 协议客户端
std::unique_ptr<RequestAdapter> request_adapter_; // 请求转换器
std::unique_ptr<ResponseAdapter> response_adapter_; // 响应转换器
};
设计分析 :A2AAdapter 内部组合了三个独立的协作组件:
A2AClient:负责 HTTP 通信(JSON-RPC over HTTP)RequestAdapter:gRPC Protobuf → A2A JSON-RPC 请求转换ResponseAdapter:A2A JSON-RPC 响应 → gRPC Protobuf 转换
这是经典的适配器模式(Adapter Pattern),将两种不兼容的协议桥接在一起。
5.2 同步查询的协议转换流程
文件:<a2a_adapter/src/a2a_adapter.cpp>
cpp
bool A2AAdapter::processQuery(
const agent_communication::AIQueryRequest& request,
agent_communication::AIQueryResponse* response) {
auto start_time = std::chrono::steady_clock::now();
try {
// Step 1: gRPC Protobuf → A2A MessageSendParams
a2a::MessageSendParams params = request_adapter_->convertToA2A(request);
// Step 2: 通过 A2A 客户端发送 HTTP 请求到 Orchestrator
a2a::A2AResponse a2a_response = a2a_client_->send_message(params);
// Step 3: A2A Response → gRPC Protobuf Response
response_adapter_->convertFromA2A(a2a_response, request.request_id(), response);
// 计算耗时
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time);
response->set_processing_time_ms(duration.count());
return true;
} catch (const a2a::A2AException& e) {
// A2A 协议错误处理
auto* status = response->mutable_status();
status->set_code(static_cast<int>(e.error_code()));
status->set_message(e.what());
return false;
} catch (const std::exception& e) {
// 通用错误处理
auto* status = response->mutable_status();
status->set_code(-1);
status->set_message(e.what());
return false;
}
}
这三步就是协议桥接的精髓:
convertToA2A()--- 将 Protobuf 问题转为 A2A 的MessageSendParamssend_message()--- 通过 HTTP POST 将 JSON-RPC 请求发给 OrchestratorconvertFromA2A()--- 将 Orchestrator 的 JSON 响应转回 Protobuf
5.3 流式查询的 SSE 转换
文件:a2a_adapter/src/a2a_adapter.cpp
cpp
void A2AAdapter::processQueryStreaming(
const agent_communication::AIQueryRequest& request,
std::function<void(const agent_communication::AIStreamEvent&)> callback) {
try {
a2a::MessageSendParams params = request_adapter_->convertToA2A(request);
std::string context_id = params.context_id().value_or("");
// ★ 使用 A2A 流式 API (SSE: Server-Sent Events)
a2a_client_->send_message_streaming(params,
[this, &callback, &context_id](const std::string& event_line) {
// 解析 SSE 格式: "data: {...}"
std::string event_data = event_line;
const std::string data_prefix = "data: ";
if (event_data.find(data_prefix) == 0) {
event_data = event_data.substr(data_prefix.length());
}
// 解析 JSON
json j = json::parse(event_data);
if (j.contains("result")) {
auto& result = j["result"];
std::string type = result.value("type", "");
if (type == "chunk") {
// 流式文本块 → gRPC "partial" 事件
agent_communication::AIStreamEvent event;
response_adapter_->buildStreamEvent(
result.value("content", ""), context_id, "partial", &event);
callback(event);
} else if (type == "stream_start") {
// 开始事件 → gRPC "status" 事件
agent_communication::AIStreamEvent event;
response_adapter_->buildStreamEvent(
"", context_id, "status", &event);
event.set_task_state("processing");
callback(event);
} else if (type == "intent") {
// 意图识别事件
agent_communication::AIStreamEvent event;
response_adapter_->buildStreamEvent(
"Intent: " + result.value("intent", ""),
context_id, "status", &event);
callback(event);
}
}
});
// 发送完成事件
agent_communication::AIStreamEvent complete_event;
response_adapter_->buildStreamEvent("", context_id, "complete", &complete_event);
callback(complete_event);
} catch (const std::exception& e) {
// 发送错误事件
agent_communication::AIStreamEvent error_event;
response_adapter_->buildStreamEvent(
e.what(), request.context_id(), "error", &error_event);
callback(error_event);
}
}
面试话术 :流式查询涉及两次协议转换 :客户端发 gRPC Server-Streaming 请求 → 服务端通过 A2AAdapter 发 HTTP SSE 请求给 Orchestrator → Orchestrator 返回 SSE 事件流 → A2AAdapter 将每个 SSE 事件转为 gRPC AIStreamEvent → 通过 gRPC 流返回给客户端。整个过程中,用户只看到回调函数被一次次调用,完全感知不到底层的协议转换。
5.4 RequestAdapter------请求方向的转换
文件:<a2a_adapter/src/request_adapter.cpp>
cpp
a2a::MessageSendParams RequestAdapter::convertToA2A(
const agent_communication::AIQueryRequest& rpc_request) {
a2a::MessageSendParams params;
// 提取或生成上下文 ID
std::string context_id = extractOrGenerateContextId(rpc_request);
params.set_context_id(context_id);
// 构建 A2A AgentMessage
a2a::AgentMessage message = buildAgentMessage(
rpc_request.question(), context_id, a2a::MessageRole::User);
params.set_message(message);
// 设置历史长度
if (rpc_request.history_length() > 0) {
params.set_history_length(rpc_request.history_length());
}
return params;
}
a2a::AgentMessage RequestAdapter::buildAgentMessage(
const std::string& content,
const std::string& context_id,
a2a::MessageRole role) {
a2a::AgentMessage message;
message.set_message_id(generateMessageId());
message.set_context_id(context_id);
message.set_role(role);
// ★ 将纯文本问题包装为 A2A TextPart(多态 Part 体系)
message.add_part(std::make_unique<a2a::TextPart>(content));
return message;
}
设计分析 :RequestAdapter 将 Protobuf 的扁平结构(question 字段)转换为 A2A 的富结构(AgentMessage + TextPart 多态 Part 体系)。这体现了两个协议在数据模型上的差异:gRPC/Protobuf 偏平、高效;A2A 偏向丰富的类型系统。
5.5 ResponseAdapter------响应方向的转换
文件:<a2a_adapter/src/response_adapter.cpp>
cpp
void ResponseAdapter::convertFromA2A(
const a2a::A2AResponse& a2a_response,
const std::string& request_id,
agent_communication::AIQueryResponse* rpc_response) {
rpc_response->set_request_id(request_id);
auto* status = rpc_response->mutable_status();
status->set_code(0);
status->set_message("Success");
if (a2a_response.is_task()) {
// ★ Task 模式:从任务历史中提取最后一条 Agent 消息
const auto& task = a2a_response.as_task();
rpc_response->set_task_id(task.id());
rpc_response->set_context_id(task.context_id());
const auto& history = task.history();
for (auto it = history.rbegin(); it != history.rend(); ++it) {
if (it->role() == a2a::MessageRole::Agent) {
rpc_response->set_answer(extractTextContent(*it));
break;
}
}
// 复制产物 (Artifacts)
for (const auto& artifact : task.artifacts()) {
auto* proto_artifact = rpc_response->add_artifacts();
proto_artifact->set_name(artifact.name());
if (artifact.mime_type().has_value()) {
proto_artifact->set_mime_type(artifact.mime_type().value());
}
}
} else if (a2a_response.is_message()) {
// ★ Message 模式:直接提取消息内容
const auto& message = a2a_response.as_message();
rpc_response->set_answer(extractTextContent(message));
}
}
提取文本内容时使用多态:
cpp
std::string ResponseAdapter::extractTextContent(const a2a::AgentMessage& message) {
std::string content;
for (const auto& part : message.parts()) {
if (part->kind() == a2a::PartKind::Text) {
auto* text_part = dynamic_cast<const a2a::TextPart*>(part.get());
if (text_part) {
if (!content.empty()) content += "\n";
content += text_part->text();
}
}
}
return content;
}
面试话术 :ResponseAdapter 需要处理两种 A2A 响应格式(Task 和 Message),因为 Orchestrator 可能返回完整的 AgentTask(包含历史记录和产物),也可能返回简单的 AgentMessage。适配器统一提取文本答案填入 Protobuf 的 answer 字段,对客户端完全透明。
5.6 ErrorMapper------错误码映射
文件:<a2a_adapter/include/agent_rpc/a2a_adapter/error_mapper.h>
cpp
class ErrorMapper {
public:
// A2A 错误码 → gRPC StatusCode
static grpc::StatusCode mapToGrpcStatus(a2a::ErrorCode a2a_code);
// 创建完整的 gRPC Status
static grpc::Status createGrpcStatus(
a2a::ErrorCode a2a_code, const std::string& message = "");
// 获取人类可读的错误描述
static std::string getErrorDescription(a2a::ErrorCode a2a_code);
};
设计分析 :两种协议的错误体系不同(A2A 用 JSON-RPC 错误码如 -32600/-32601,gRPC 用 StatusCode 枚举如 UNAVAILABLE/NOT_FOUND),ErrorMapper 负责这种"错误语义"的无损转换。
5.7 A2AConfig------后端配置
文件:<a2a_adapter/include/agent_rpc/a2a_adapter/a2a_config.h>
cpp
struct A2AConfig {
// Orchestrator 地址
std::string orchestrator_url = "http://localhost:5000";
int orchestrator_port = 5000;
// 服务注册中心
std::string registry_url = "http://localhost:8500";
int heartbeat_interval_seconds = 30;
// Redis 任务存储
bool enable_redis_store = false;
std::string redis_url = "localhost:6379";
// 请求配置
int request_timeout_seconds = 30;
int history_length = 10;
int max_retries = 3;
int retry_delay_ms = 1000;
// 特性开关
bool enable_streaming = true;
bool enable_metrics = true;
bool validate(); // 参数校验 + 自动修正
};
六、AIQueryServiceImpl------服务端的请求处理中枢
文件:<server/include/agent_rpc/server/ai_query_service.h>
AIQueryServiceImpl 是 gRPC 生成的 AIQueryService::Service 的实现,它是整个"超级智能体"对外的唯一服务端入口:
cpp
class AIQueryServiceImpl final : public agent_communication::AIQueryService::Service {
public:
// 初始化时创建 A2AAdapter
bool initialize(const common::RpcConfig& rpc_config,
const a2a_adapter::A2AConfig& a2a_config);
// ★ 三个 gRPC 方法的实现
grpc::Status Query(...) override; // 同步查询
grpc::Status QueryStream(...) override; // 流式查询
grpc::Status GetQueryStatus(...) override; // 状态查询
private:
// ★ 核心:持有 A2AAdapter,将请求桥接到 A2A 协议
std::unique_ptr<a2a_adapter::A2AAdapter> a2a_adapter_;
};
文件:<server/src/ai_query_service.cpp>
同步查询的实现:
cpp
grpc::Status AIQueryServiceImpl::Query(
grpc::ServerContext* context,
const agent_communication::AIQueryRequest* request,
agent_communication::AIQueryResponse* response) {
if (!isAvailable()) {
return grpc::Status(grpc::StatusCode::UNAVAILABLE,
"AI Query Service not available");
}
auto start_time = std::chrono::steady_clock::now();
std::string request_id = request->request_id().empty()
? generateRequestId() : request->request_id();
if (context->IsCancelled()) {
return grpc::Status(grpc::StatusCode::CANCELLED, "Request cancelled");
}
// ★ 核心调用:将 gRPC 请求委托给 A2AAdapter 处理
bool success = a2a_adapter_->processQuery(*request, response);
response->set_request_id(request_id);
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time);
recordMetrics("Query", duration.count(), success);
if (success) {
return grpc::Status::OK;
} else {
return grpc::Status(grpc::StatusCode::INTERNAL, response->status().message());
}
}
设计分析 :AIQueryServiceImpl 的实现非常薄------它只做了三件事:
- 参数校验和取消检查
- 将请求完全委托给
A2AAdapter - 记录指标和返回状态
这体现了单一职责原则:gRPC 服务层只处理 gRPC 关注点(context、deadline、cancellation),业务逻辑全部由适配器层处理。
七、Orchestrator------真正的"大脑"
7.1 Orchestrator 做了什么
文件:<examples/ai_orchestrator/orchestrator_main.cpp>
AIOrchestrator 是整个系统的核心调度者,它:
- 接收 A2A 协议请求(JSON-RPC over HTTP)
- 意图识别:用 LLM(通义千问)判断用户意图(math/code/general)
- 路由调度:将请求路由到合适的专业 Agent
- 工具增强:通过 MCP 集成调用外部工具
- 返回结果:将处理结果通过 A2A 协议返回
cpp
class AIOrchestrator {
public:
AIOrchestrator(const std::string& agent_id,
const std::string& listen_address,
const std::string& registry_url,
const std::string& api_key,
const std::string& redis_host, int redis_port,
const MCPAgentConfig& mcp_config)
: task_store_(std::make_shared<RedisTaskStore>(redis_host, redis_port))
, qwen_client_(api_key) // LLM 客户端
, registry_client_(registry_url) // 服务发现客户端
, mcp_integration_(std::make_unique<MCPAgentIntegration>()) // MCP 工具
{
// 初始化 MCP
if (!mcp_integration_->initialize(mcp_config)) {
std::cerr << "MCP 初始化失败,将在无 MCP 模式下运行" << std::endl;
}
}
...
};
7.2 意图识别与路由
cpp
std::string analyze_intent(const std::string& text) {
std::string prompt = "判断以下用户输入属于哪个类别,只回答类别名称:\n"
"- math: 数学计算、方程求解\n"
"- code: 编程、代码相关\n"
"- general: 其他对话\n\n"
"用户输入: " + text;
std::string result = qwen_client_.chat("", prompt);
if (result.find("math") != std::string::npos) return "math";
if (result.find("code") != std::string::npos) return "code";
return "general";
}
根据意图路由到不同处理逻辑:
cpp
std::string handle_request(const std::string& body) {
auto message = AgentMessage::from_json(params_json["message"].dump());
std::string user_text = /* 从 TextPart 提取 */;
// ★ 意图识别
std::string intent = analyze_intent(user_text);
std::string response_text;
if (intent == "math") {
response_text = call_math_agent(user_text, context_id); // → MathAgent
} else if (intent == "code") {
response_text = call_code_agent(user_text, context_id); // → CodeAgent
} else {
response_text = handle_general_query(user_text, context_id); // → LLM + MCP
}
// 返回 A2A 协议响应
auto response = JsonRpcResponse::create_success(request.id(), response_msg.to_json());
return response.to_json();
}
7.3 通过 Registry 发现专业 Agent
cpp
std::string call_agent_by_tag(const std::string& tag,
const std::string& query,
const std::string& context_id) {
// ★ 从注册中心动态查找 Agent
std::string agent_url = registry_client_.select_agent_by_tag(tag);
// 构造 A2A JSON-RPC 请求
json request = {
{"jsonrpc", "2.0"},
{"method", "message/send"},
{"params", {
{"message", {
{"role", "user"},
{"contextId", context_id},
{"parts", {{{"kind", "text"}, {"text", query}}}}
}},
{"historyLength", 5}
}}
};
// HTTP POST 发送给 Agent
std::string response_body = SimpleHttpClient::post(agent_url, request.dump());
...
}
7.4 MCP 工具增强通用查询
cpp
std::string handle_general_query(const std::string& query, const std::string& context_id) {
// 获取对话历史
auto history = task_store_->get_history(context_id, 5);
// ★ 尝试 MCP 工具增强
std::string tool_context = tryMCPTools(query);
if (!tool_context.empty()) {
history_text += "\n工具辅助信息:\n" + tool_context;
}
// 调用 LLM 回答
return qwen_client_.chat(history_text, query);
}
面试话术:Orchestrator 是一个"元 Agent"------它自己不直接回答问题,而是通过 LLM 识别用户意图,然后路由到专业 Agent 或使用 MCP 工具辅助回答。用户看到的"超级 AI 智能体",实际上是 Orchestrator 在背后协调多个组件的结果。
八、系统启动脚本------一键启动完整系统
文件:<examples/ai_orchestrator/start_system.sh>
bash
# 启动顺序: Registry → Math Agent → Orchestrator
# 1. 启动 Registry Server (服务注册中心)
"$BIN_DIR/ai_registry_server" $REGISTRY_PORT &
# 2. 启动 Math Agent (数学专业 Agent)
"$BIN_DIR/ai_math_agent" math-1 $MATH_AGENT_PORT \
http://localhost:$REGISTRY_PORT $API_KEY \
--redis-host $REDIS_HOST --redis-port $REDIS_PORT \
$MCP_ARGS $RAG_ARGS &
# 3. 启动 Orchestrator (调度器)
"$BIN_DIR/ai_orchestrator" orch-1 $ORCHESTRATOR_PORT \
http://localhost:$REGISTRY_PORT $API_KEY \
--redis-host $REDIS_HOST --redis-port $REDIS_PORT \
$MCP_ARGS $RAG_ARGS &
加上 RPC Server 和 RPC Client,完整的启动流程是:
start_system.sh # 启动 Registry + MathAgent + Orchestrator
./rpc_server # 启动 gRPC 服务端 (连接到 Orchestrator)
./rpc_client localhost:50051 # 用户客户端
对用户来说 :只需启动 rpc_client 并连接到 rpc_server 的地址即可。Registry、Orchestrator、MathAgent、MCP Server 等全部在后台运行,用户完全不感知。
九、用户端使用体验------极简交互
文件:<client/src/main.cpp>
cpp
int main(int argc, char* argv[]) {
std::string server_address = "localhost:50051";
// ★ 两行代码完成连接
AIQueryClient client;
client.connect(server_address);
// 交互式对话循环
while (g_running) {
std::cout << "> ";
std::getline(std::cin, line);
if (stream_mode) {
// 流式:逐字输出
client.queryStream(line,
[](const agent_communication::AIStreamEvent& event) {
if (event.event_type() == "partial") {
std::cout << event.content();
} else if (event.event_type() == "complete") {
std::cout << std::endl;
}
}, context_id, timeout_seconds);
} else {
// 同步:完整输出
auto response = client.query(line, context_id, timeout_seconds);
if (response.status().code() == 0) {
std::cout << "\nAI: " << response.answer() << std::endl;
if (!response.agent_name().empty()) {
std::cout << "[Agent: " << response.agent_name()
<< ", 耗时: " << response.processing_time_ms() << "ms]"
<< std::endl;
}
}
}
}
}
用户视角:就像在用一个本地 ChatGPT。输入问题,得到回答。不知道背后有 gRPC → A2A 适配 → Orchestrator 意图识别 → Agent 路由 → MCP 工具调用 → LLM 推理 这一整条链路。
十、完整调用链路(以一次数学查询为例)
用户输入: "计算 3+5*2 等于多少"
|
v
[RpcClient] aiQuery("计算 3+5*2 等于多少")
| 组合委托
v
[AIQueryClient] stub_->Query(request, &response)
| gRPC Protobuf
v
[AIQueryServiceImpl] Query() → a2a_adapter_->processQuery(request, response)
| 委托给 A2AAdapter
v
[A2AAdapter] processQuery()
| 1. request_adapter_->convertToA2A() ← Protobuf → A2A Message
| 2. a2a_client_->send_message(params) ← HTTP POST JSON-RPC
| 3. response_adapter_->convertFromA2A() ← A2A → Protobuf
v
[Orchestrator] handle_request()
| 1. 解析 A2A JSON-RPC 请求
| 2. analyze_intent("计算 3+5*2") → "math" ← 调用 LLM 意图识别
| 3. call_math_agent(query) ← 从 Registry 查找 MathAgent
v
[MathAgent] handle_request()
| 处理数学计算请求
| 返回 A2A JSON-RPC 响应
v
(结果沿调用链反向传播)
|
v
response.answer() = "3+5*2 = 13"
十一、连接可靠性------重连与心跳
文件:<client/src/rpc_client.cpp>
cpp
bool RpcClient::reconnect() {
if (connection_retry_count_ >= MAX_RETRY_COUNT) { // 最多 5 次
LOG_ERROR("Max reconnection attempts reached");
return false;
}
// 指数退避:延迟 = RETRY_DELAY_MS * (重试次数 + 1)
std::this_thread::sleep_for(
std::chrono::milliseconds(RETRY_DELAY_MS * (connection_retry_count_ + 1)));
try {
setupChannel();
connected_ = true;
connection_retry_count_ = 0;
return true;
} catch (...) {
connection_retry_count_++;
return false;
}
}
void RpcClient::heartbeatLoop() {
while (heartbeat_running_) {
if (connected_ && !current_agent_id_.empty()) {
if (!sendHeartbeat(current_agent_id_, current_agent_info_)) {
LOG_WARN("Heartbeat failed, attempting reconnection");
connected_ = false;
if (!reconnect()) {
LOG_ERROR("Failed to reconnect, stopping heartbeat");
break;
}
}
}
std::this_thread::sleep_for(std::chrono::seconds(config_.heartbeat_interval));
}
}
服务端的 Agent 清理机制:
文件:<server/src/agent_service.cpp>
cpp
void AgentCommunicationServiceImpl::cleanupOfflineAgents() {
std::lock_guard<std::mutex> lock(agents_mutex_);
auto now = std::chrono::steady_clock::now();
auto timeout = std::chrono::seconds(60); // 60秒未心跳视为下线
auto it = agents_.begin();
while (it != agents_.end()) {
if (now - it->second.last_heartbeat > timeout) {
LOG_WARN("Agent offline, removing: " + it->first);
agent_message_queues_.erase(it->first);
it = agents_.erase(it);
} else {
++it;
}
}
}
十二、设计模式总结
| 模式 | 应用位置 | 作用 |
|---|---|---|
| 门面模式 (Facade) | RpcClient |
统一入口,隐藏内部复杂性 |
| 适配器模式 (Adapter) | A2AAdapter + RequestAdapter + ResponseAdapter |
gRPC ↔ A2A 协议桥接 |
| 组合模式 (Composition) | RpcClient 持有 AIQueryClient |
组合优于继承 |
| 委托模式 (Delegation) | AIQueryServiceImpl → A2AAdapter |
服务层委托业务逻辑给适配器层 |
| 建造者模式 (Builder) | grpc::ServerBuilder |
多步构建 gRPC Server |
| 策略模式 (Strategy) | Orchestrator 意图路由 | 不同意图对应不同处理策略 |
| 观察者/回调模式 | StreamEventCallback |
流式事件通知 |
十三、面试话术
Q1: 请介绍一下这个项目的整体架构
这个项目是一个基于 C++17 和 gRPC 的高性能 AI Agent 通信框架,采用分层架构设计。对用户来说只有一个
RpcClient入口,调用aiQuery()就能与 AI 对话。底层实际是:RpcClient 通过 gRPC 调用 RpcServer,RpcServer 内部的 AIQueryServiceImpl 通过 A2AAdapter 将请求转为 A2A 协议发给 Orchestrator,Orchestrator 用 LLM 做意图识别后路由到专业 Agent(如 MathAgent),或者用 MCP 工具增强回答。整个系统看起来像一个"超级 AI 智能体",实际上是多个 Agent 和多种工具在协同工作。
Q2: 适配器层为什么要独立出来?
A2AAdapter 层独立出来有三个原因:第一,解耦协议 ------gRPC/Protobuf 和 A2A/JSON-RPC 是两种完全不同的协议体系,错误码、消息格式、交互模式都不同,需要专门的转换逻辑。第二,可测试性 ------适配器层可以独立于 gRPC 和 Orchestrator 进行单元测试。第三,可替换性------如果将来要支持 REST API 或 WebSocket 替代 gRPC,只需要修改适配器层,不影响 Orchestrator 端。
Q3: 流式查询是怎么贯穿整个系统的?
流式查询涉及两次协议转换:客户端发起 gRPC Server-Streaming RPC,服务端的 AIQueryServiceImpl 调用 A2AAdapter 的
processQueryStreaming方法,A2AAdapter 通过 A2AClient 向 Orchestrator 发起 HTTP SSE(Server-Sent Events)请求。Orchestrator 将响应分成多个 JSON chunk 通过 SSE 返回,A2AAdapter 的回调函数解析每个 SSE 事件,转换为 gRPC 的AIStreamEvent,写入 gRPC 流。客户端通过ClientReader逐个读取事件并调用用户的回调函数。
Q4: 为什么用户看不到底层的多 Agent 架构?
这是通过三层封装 实现的:第一层是
RpcClient的门面封装,它内部组合了AIQueryClient,但对外只暴露aiQuery()和aiQueryStream()两个方法;第二层是 gRPC 服务端的封装,RpcServer把AIQueryServiceImpl、健康检查等多个服务注册到同一个端口;第三层是 A2AAdapter 的协议封装,它将 Orchestrator 的意图识别、Agent 路由、MCP 工具调用等全部隐藏在一个processQuery()调用之后。用户从头到尾只与 Protobuf 定义的AIQueryRequest/Response交互。
十四、延伸思考
-
如果请求量很大,A2AAdapter 到 Orchestrator 的 HTTP 连接会成为瓶颈吗?
- 当前实现是同步 HTTP,是会成为瓶颈。可以考虑 HTTP 连接池、异步 I/O(如 libcurl multi)或直接用 gRPC 替代 HTTP。
-
为什么不直接让 RpcServer 内置 Orchestrator 逻辑,而是要走一次 HTTP 调用?
- 分离部署的灵活性。Orchestrator 可以独立扩缩容,可以运行在不同机器上。A2A 作为标准协议,允许 Orchestrator 被其他 A2A 兼容的客户端直接调用。
-
如何保证端到端的 context_id 一致性?
RequestAdapter在extractOrGenerateContextId()中首先检查用户是否提供了 context_id,有则复用,无则自动生成。这个 ID 会一路传递到 Orchestrator 和 Redis,保证多轮对话的上下文追踪。