AI_agent(三) MCP协议(二)

插件系统实现:动态库加载 + 统一 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 接口定义

文件:mcp_server_integrated/src/interface/PluginAPI.h

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 跨平台类型定义

文件:mcp_server_integrated/src/loader/PluginsLoader.h

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:扫描目录递归查找动态库

文件:mcp_server_integrated/src/loader/PluginsLoader.cpp

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 类插件

文件:mcp_server_integrated/plugins/calculator/Calculator.cpp

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 编译为动态库

文件:mcp_server_integrated/plugins/calculator/CMakeLists.txt

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 中的完整流程

文件:mcp_server_integrated/src/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/listtools/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:方法名 → 处理函数的映射表

文件:mcp_server_integrated/src/server/Server.cpp 构造函数

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}}

八、已有插件一览

目录:mcp_server_integrated/plugins/

插件 文件 类型 工具 说明
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 测试长时间运行的任务

所有插件都遵循同一个模式:

  1. 定义 static PluginTool methods[] 数组
  2. 实现 GetName/GetVersion/GetType/Initialize/HandleRequest/Shutdown
  3. 实现 GetToolCount/GetTool(或 GetPromptCount/GetPrompt 等)
  4. 组装 static PluginAPI plugin = { ... }
  5. 导出 extern "C" CreatePlugin() / DestroyPlugin()
  6. 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/listprompts/listresources/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 外部调用)

文件:mcp_server_integrated/plugins/weather/Weather.cpp

这是最典型的**"有外部 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 的作用

  1. 给 LLM 看 :LLM(如通义千问)看到 inputSchema 后,知道应该把参数组织成 {"latitude":"39.9", "longitude":"116.4", "city":"Beijing"} 的格式
  2. 给客户端校验:客户端可以在发送前用 JSON Schema 验证参数合法性
  3. 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:mcp_server_integrated/plugins/weather/CMakeLists.txt

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 类型)

文件:mcp_server_integrated/plugins/code-review/CodeReview.cpp

这是项目中唯一的 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++"
      }
    }
  ]
}

客户端收到这个消息模板后,可以:

  1. messages 数组中追加用户的代码内容
  2. 把整个 messages 发给 LLM(如通义千问)
  3. 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 类型)

文件:mcp_server_integrated/plugins/bacio-quote/BacioQuote.cpp

这是项目中唯一的 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 类型
    }
};

PluginResourcePluginTool 的关键区别

  • PluginToolinputSchema(参数 Schema)→ 用于执行
  • PluginResourceuri + 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 辅助函数

文件:mcp_server_integrated/src/utils/MCPBuilder.h

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 类型)

文件:mcp_server_integrated/plugins/sleep/Sleep.cpp

最简单的插件,完整代码不到 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/callprompts/getresources/read 对应的处理逻辑。
面试官:Weather 插件的 HTTP 请求是同步的,这不会阻塞主服务吗?

确实,当前 Weather 插件中 httplib::Client::Get() 是同步阻塞的。但这不会阻塞整个主服务,因为请求处理发生在主服务为该客户端连接分配的线程中(STDIO transport 是单连接的主线程,SSE transport 的每个请求由 httplib 的线程池处理)。其他客户端的请求不受影响。

如果需要优化,可以:

  1. 在插件内部使用 std::async 或线程池异步发起 HTTP 请求
  2. 对 httplib::Client 设置超时(cli.set_read_timeout(5, 0)
  3. 添加缓存层,对同一城市的短时间内重复查询直接返回缓存结果
    面试官: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 生成向量

头文件:mcp/include/agent_rpc/mcp/rag/embedding_service.h

实现:mcp/src/rag/embedding_service.cpp

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:内存向量索引

头文件:mcp/include/agent_rpc/mcp/rag/vector_index.h

实现:mcp/src/rag/vector_index.cpp

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 缓存

头文件:mcp/include/agent_rpc/mcp/rag/embedding_cache.h

实现:mcp/src/rag/embedding_cache.cpp

5.1 为什么需要缓存?

同一个查询文本(如 "计算 123 + 456")可能被多次发送到 Embedding API。缓存可以:

  1. 减少 API 调用次数(DashScope Embedding 按调用次数计费)
  2. 降低延迟(缓存命中时直接返回,不用等 HTTP 往返)
  3. 减轻 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:检索器(整合三大组件)

头文件:mcp/include/agent_rpc/mcp/rag/tool_retriever.h

实现:mcp/src/rag/tool_retriever.cpp

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:统一集成入口与降级机制

头文件:mcp/include/agent_rpc/mcp/mcp_agent_integration.h

实现:mcp/src/mcp_agent_integration.cpp

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();
    }
}

两层降级保护

  1. RAG 未启用(!isRAGEnabled())→ 返回全部工具
  2. 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:可选的工具验证

文件:mcp/src/rag/tool_validator.cpp

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)

文件:tests/test_rag_mcp_properties.cpp

项目使用 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:调用阿里百炼大模型

文件:a2a/include/a2a/examples/qwen_client.hpp

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 协议集成入口

头文件:mcp/include/agent_rpc/mcp/mcp_agent_integration.h

实现:mcp/src/mcp_agent_integration.cpp

这是连接 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 完整集成

文件:examples/ai_orchestrator/math_agent_main.cpp

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 的三层容错

  1. RAG 检索 → 从语义相似度最高的工具中找 calculator
  2. RAG 降级 → RAG 未启用时,手动检查 calculator 是否存在
  3. 直接回退 → RAG 检索结果中没有 calculator,直接按名字尝试

五、AIOrchestrator:意图识别 + 工具增强

文件:examples/ai_orchestrator/orchestrator_main.cpp

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:另一种集成方式

文件:examples/grpc_ai_demo/grpc_server_main.cpp

这是通过 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 集成测试

文件:examples/rpc_mcp_integration_test.cpp

这是一个独立的集成测试程序,验证 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 客户端连接
}

八、系统启动脚本:一键部署

文件:examples/ai_orchestrator/start_system.sh

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 的角色

头文件:mcp/include/agent_rpc/mcp/mcp_client.h

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 单元测试

文件:tests/test_mcp_integration.cpp

测试分为两组:

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/list JSON-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,遵循"组合优于继承"原则。好处是:

  1. AIQueryClient 可以独立测试和复用
  2. RpcClient 可以随时替换底层 AI 查询实现
  3. 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_idagent_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;
    }
}

这三步就是协议桥接的精髓

  1. convertToA2A() --- 将 Protobuf 问题转为 A2A 的 MessageSendParams
  2. send_message() --- 通过 HTTP POST 将 JSON-RPC 请求发给 Orchestrator
  3. convertFromA2A() --- 将 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 的实现非常薄------它只做了三件事:

  1. 参数校验和取消检查
  2. 将请求完全委托给 A2AAdapter
  3. 记录指标和返回状态

这体现了单一职责原则:gRPC 服务层只处理 gRPC 关注点(context、deadline、cancellation),业务逻辑全部由适配器层处理。


七、Orchestrator------真正的"大脑"
7.1 Orchestrator 做了什么

文件:<examples/ai_orchestrator/orchestrator_main.cpp>

AIOrchestrator 是整个系统的核心调度者,它:

  1. 接收 A2A 协议请求(JSON-RPC over HTTP)
  2. 意图识别:用 LLM(通义千问)判断用户意图(math/code/general)
  3. 路由调度:将请求路由到合适的专业 Agent
  4. 工具增强:通过 MCP 集成调用外部工具
  5. 返回结果:将处理结果通过 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) AIQueryServiceImplA2AAdapter 服务层委托业务逻辑给适配器层
建造者模式 (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 服务端的封装,RpcServerAIQueryServiceImpl、健康检查等多个服务注册到同一个端口;第三层是 A2AAdapter 的协议封装,它将 Orchestrator 的意图识别、Agent 路由、MCP 工具调用等全部隐藏在一个 processQuery() 调用之后。用户从头到尾只与 Protobuf 定义的 AIQueryRequest/Response 交互。


十四、延伸思考
  1. 如果请求量很大,A2AAdapter 到 Orchestrator 的 HTTP 连接会成为瓶颈吗?

    • 当前实现是同步 HTTP,是会成为瓶颈。可以考虑 HTTP 连接池、异步 I/O(如 libcurl multi)或直接用 gRPC 替代 HTTP。
  2. 为什么不直接让 RpcServer 内置 Orchestrator 逻辑,而是要走一次 HTTP 调用?

    • 分离部署的灵活性。Orchestrator 可以独立扩缩容,可以运行在不同机器上。A2A 作为标准协议,允许 Orchestrator 被其他 A2A 兼容的客户端直接调用。
  3. 如何保证端到端的 context_id 一致性?

    • RequestAdapterextractOrGenerateContextId() 中首先检查用户是否提供了 context_id,有则复用,无则自动生成。这个 ID 会一路传递到 Orchestrator 和 Redis,保证多轮对话的上下文追踪。
相关推荐
无人装备硬件开发爱好者1 小时前
硬核技术解析|MCP 协议实现语音 AI 与 ESP32 软 / 硬件的标准化对接:从火山引擎豆包认证到全链路落地——上
人工智能·esp32·火山引擎·mcp
乐观勇敢坚强的老彭2 小时前
c++寒假营day05
开发语言·c++·算法
枫叶丹42 小时前
【Qt开发】Qt界面优化(七)-> Qt样式表(QSS) 样式属性
c语言·开发语言·c++·qt
mantch2 小时前
全网最全 Claude Skills 指南:从原理到应用,一篇搞定!
人工智能·aigc·agent
阿里云云原生2 小时前
Agent 越用越聪明?AgentScope Java 在线训练插件来了!
前端·agent
小马过河R2 小时前
OpenClaw 记忆系统工作原理
人工智能·机器学习·语言模型·agent·openclaw·智能体记忆机制
码农垦荒笔记3 小时前
OpenClaw 实战#05-5:第五层工程拆解——Skill 工程设计规范(硬干货版)
人工智能·agent·设计规范·openclaw
宵时待雨3 小时前
C++笔记归纳2:类和对象
c++·笔记
带刺的坐椅3 小时前
Solon AI v3.9.4 发布(智能体开发框架,支持 Java8 到 Java25)
ai·llm·agent·solon·mcp·tool-call·skills