第 8 节 C++ 设计模式在 AI 推理 SDK 和 Agent 工具运行时中如何落地

目录

  • [8.1 设计模式在 AI 工程中的价值](#8.1 设计模式在 AI 工程中的价值)
  • [8.2 创建型模式](#8.2 创建型模式)
    • [8.2.1 工厂方法与抽象工厂](#8.2.1 工厂方法与抽象工厂)
    • [8.2.2 建造者(Builder)](#8.2.2 建造者(Builder))
    • [8.2.3 单例(Singleton)](#8.2.3 单例(Singleton))
    • [8.2.4 原型(Prototype)](#8.2.4 原型(Prototype))
  • [8.3 结构型模式](#8.3 结构型模式)
    • [8.3.1 适配器(Adapter)](#8.3.1 适配器(Adapter))
    • [8.3.2 装饰器(Decorator)](#8.3.2 装饰器(Decorator))
    • [8.3.3 代理(Proxy)](#8.3.3 代理(Proxy))
    • [8.3.4 外观(Facade)](#8.3.4 外观(Facade))
    • [8.3.5 桥接(Bridge)](#8.3.5 桥接(Bridge))
    • [8.3.6 组合(Composite)](#8.3.6 组合(Composite))
    • [8.3.7 享元(Flyweight)](#8.3.7 享元(Flyweight))
  • [8.4 行为型模式](#8.4 行为型模式)
    • [8.4.1 策略(Strategy)](#8.4.1 策略(Strategy))
    • [8.4.2 观察者(Observer)/ 发布订阅](#8.4.2 观察者(Observer)/ 发布订阅)
    • [8.4.3 责任链(Chain of Responsibility)](#8.4.3 责任链(Chain of Responsibility))
    • [8.4.4 模板方法(Template Method)](#8.4.4 模板方法(Template Method))
    • [8.4.5 迭代器(Iterator)](#8.4.5 迭代器(Iterator))
    • [8.4.6 状态(State)](#8.4.6 状态(State))
    • [8.4.7 命令(Command)](#8.4.7 命令(Command))
    • [8.4.8 访问者(Visitor)](#8.4.8 访问者(Visitor))
    • [8.4.9 中介者(Mediator)](#8.4.9 中介者(Mediator))
  • [8.5 RAII:C++ 特有的资源管理模式](#8.5 RAII:C++ 特有的资源管理模式)
  • [8.6 Pimpl:编译防火墙与 ABI 稳定](#8.6 Pimpl:编译防火墙与 ABI 稳定)
  • [8.7 类型擦除:统一不同签名的工具函数](#8.7 类型擦除:统一不同签名的工具函数)
  • [8.8 Agent 工具运行时场景专题](#8.8 Agent 工具运行时场景专题)
  • [8.9 设计模式组合使用](#8.9 设计模式组合使用)
  • [8.10 反模式与过度设计](#8.10 反模式与过度设计)
  • [8.11 CRTP 静态多态](#8.11 CRTP 静态多态)
  • [8.12 易错点分析](#8.12 易错点分析)
  • [8.13 面试追问与回答](#8.13 面试追问与回答)
  • [8.14 本节总结](#8.14 本节总结)

**摘要:**本节系统讲解 C++ 设计模式在 AI 推理 SDK 和 Agent 工具运行时中的落地实践。创建型模式(工厂、建造者、单例、原型)解决对象构造的灵活性与一致性;结构型模式(适配器、装饰器、代理、外观、桥接、组合、享元)解决多后端 API 统一与功能叠加;行为型模式(策略、观察者、责任链、模板方法、状态、命令、访问者、中介者)解决推理流程编排与工具调用链;RAII、Pimpl、类型擦除、CRTP 是 C++ 特有的工程基石。文中给出大量可编译 C++ 示例、面试追问速查表、模式选型决策流程图,并总结易错点与反模式,帮助读者在真实工程中做出合理选型。

第 8 节 C++ 设计模式在 AI 推理 SDK 和 Agent 工具运行时中如何落地?


【一句话核心概括】设计模式在 AI 工程中的价值不在于套用经典模板,而在于针对推理 SDK 的多后端扩展性、性能敏感性和 Agent 运行时的工具动态性、事件驱动性,选择合适的模式组合 ------ 创建型解决对象构造的灵活性,结构型解决多后端 API 统一和功能叠加,行为型解决推理流程编排和工具调用链,RAII 和 Pimpl 则是 C++ 特有的工程基石。

8.1 设计模式在 AI 工程中的价值

设计模式是前人在面对特定类型的软件设计问题时总结出的可复用解决方案。在 AI 推理 SDK 和 Agent 工具运行时的开发中,设计模式不是装饰性的 "高级技巧",而是解决真实工程痛点的必要手段。

AI 推理 SDK 面临的核心挑战包括:后端多样性(TensorRT、ONNX Runtime、OpenVINO、自研推理引擎各有各的 API 和内存模型)、硬件多样性(CPU、GPU、NPU、FPGA 需要不同的执行路径)、性能敏感性(推理延迟直接影响用户体验,热路径上的每一纳秒都重要)、资源管理复杂性(设备内存、模型权重、线程池等资源需要精细管理)。

Agent 工具运行时面临的核心挑战包括:工具动态性(工具可以在运行时注册和卸载)、调用链复杂性(一个工具调用可能经过鉴权、限流、参数校验、执行、结果格式化多个环节)、事件驱动性(工具执行前后、LLM 调用前后都需要通知和扩展点)、上下文管理(对话上下文、工具调用上下文需要在多轮交互中维护)。

下面的关系图展示了设计模式在 AI 系统中的分层应用:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                     AI推理SDK + Agent运行时                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  创建型层        工厂方法/抽象工厂  → 后端/算子/张量的动态创建    │
│                  建造者           → 推理配置的链式构建            │
│                  单例             → 注册表/日志器/设备管理器      │
│                  原型             → 模型配置/张量模板克隆         │
│                                                                   │
│  结构型层        适配器           → 多后端API统一到统一接口       │
│                  装饰器           → 日志/计时/重试/缓存层层包裹   │
│                  代理             → 远程推理/懒加载/权限控制      │
│                  外观             → SDK顶层类简化内部复杂度       │
│                  桥接             → 抽象请求与实现执行器分离      │
│                  组合             → 计算图节点与子图统一处理      │
│                  享元             → 张量描述符/算子元信息共享     │
│                                                                   │
│  行为型层        策略             → 执行/内存分配/精度校准策略    │
│                  观察者           → 推理事件/工具事件通知         │
│                  责任链           → 请求处理链/工具调用链         │
│                  模板方法         → 后端推理流程骨架              │
│                  状态             → 推理请求状态机                │
│                  命令             → 工具调用封装为可重试命令      │
│                  访问者           → 计算图优化pass                │
│                  中介者           → Agent工具间协调               │
│                                                                   │
│  C++特有        RAII             → 设备内存/锁/文件句柄管理      │
│                  Pimpl            → 编译防火墙/ABI稳定            │
│                  类型擦除         → 统一不同签名的工具函数        │
│                  CRTP             → 热路径零开销静态多态          │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8.2 创建型模式

8.2.1 工厂方法与抽象工厂

工厂模式是 AI 推理 SDK 中最基础也最重要的创建型模式。推理引擎需要支持多种后端(CPU、GPU、NPU),每种后端有自己的张量实现、算子实现和内存管理。如果在代码中到处写if (device == "cpu") return new CPUTensor(); else if (device == "gpu") return new GPUTensor();,不仅代码重复,而且新增后端时需要修改所有这些分支,违反开闭原则。

抽象工厂模式提供了一个创建一系列相关对象的接口,每个具体工厂负责创建一整套后端相关的对象。IBackendFactory定义了createTensorcreateOperatorcreateMemoryAllocator等接口,CPUBackendFactoryGPUBackendFactory分别创建 CPU 和 GPU 版本的对象。客户端只依赖抽象工厂接口,通过配置或注册表获取具体工厂实例,完全不感知后端差异。

可编译代码示例:

复制代码
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
// 抽象产品:张量
class ITensor {
public:
    virtual ~ITensor() = default;
    virtual void allocate(size_t bytes) = 0;
    virtual std::string device() const = 0;
};
class CPUTensor : public ITensor {
public:
    void allocate(size_t bytes) override { data_.resize(bytes); }
    std::string device() const override { return "CPU"; }
private:
    std::vector<char> data_;
};
class GPUTensor : public ITensor {
public:
    void allocate(size_t bytes) override { /* cudaMalloc(&gpu_ptr_, bytes); */ bytes_ = bytes; }
    std::string device() const override { return "GPU"; }
private:
    void* gpu_ptr_ = nullptr;
    size_t bytes_ = 0;
};
// 抽象工厂
class IBackendFactory {
public:
    virtual ~IBackendFactory() = default;
    virtual std::unique_ptr<ITensor> createTensor() = 0;
    virtual std::string name() const = 0;
};
class CPUBackendFactory : public IBackendFactory {
public:
    std::unique_ptr<ITensor> createTensor() override { return std::make_unique<CPUTensor>(); }
    std::string name() const override { return "CPU"; }
};
class GPUBackendFactory : public IBackendFactory {
public:
    std::unique_ptr<ITensor> createTensor() override { return std::make_unique<GPUTensor>(); }
    std::string name() const override { return "GPU"; }
};
// 工厂注册表
class FactoryRegistry {
public:
    static FactoryRegistry& instance() {
        static FactoryRegistry inst;
        return inst;
    }
    void register_factory(const std::string& name, std::unique_ptr<IBackendFactory> factory) {
        factories_[name] = std::move(factory);
    }
    IBackendFactory* get(const std::string& name) {
        auto it = factories_.find(name);
        return it == factories_.end() ? nullptr : it->second.get();
    }
private:
    std::unordered_map<std::string, std::unique_ptr<IBackendFactory>> factories_;
};
int main() {
    FactoryRegistry::instance().register_factory("CPU", std::make_unique<CPUBackendFactory>());
    FactoryRegistry::instance().register_factory("GPU", std::make_unique<GPUBackendFactory>());
    auto* factory = FactoryRegistry::instance().get("GPU");
    auto tensor = factory->createTensor();
    tensor->allocate(1024);
    std::cout << "Tensor on " << tensor->device() << std::endl;
    return 0;
}
8.2.2 建造者(Builder)

推理服务的配置项非常多:精度(FP32/FP16/INT8)、线程数、设备类型、批处理大小、缓存策略、超时时间、日志级别等。如果用构造函数传递所有参数,会出现 "参数地狱"------ 构造函数有十几个参数,调用时很难记住每个参数的含义,且参数顺序容易出错。

建造者模式通过链式调用逐步构建配置对象,每个设置方法返回*this引用,支持流畅的链式调用。InferConfigBuilder提供setPrecisionsetThreadCountsetDeviceenableCache等方法,最后调用build()生成不可变的InferConfig对象。这种方式的优势是:参数名即方法名,可读性强;可以设置默认值,只覆盖需要修改的参数;build () 时可以做参数校验(如 INT8 精度需要校准数据)。

可编译代码示例:

复制代码
#include <iostream>
#include <string>
class InferConfig {
public:
    enum class Precision { FP32, FP16, INT8 };
    enum class Device { CPU, GPU, NPU };
    Precision precision = Precision::FP32;
    int thread_count = 1;
    Device device = Device::CPU;
    size_t batch_size = 1;
    bool enable_cache = false;
    int timeout_ms = 5000;
    void print() const {
        std::cout << "Precision: " << static_cast<int>(precision)
                  << ", Threads: " << thread_count
                  << ", Device: " << static_cast<int>(device)
                  << ", Batch: " << batch_size
                  << ", Cache: " << enable_cache
                  << ", Timeout: " << timeout_ms << "ms\n";
    }
};
class InferConfigBuilder {
public:
    InferConfigBuilder& setPrecision(InferConfig::Precision p) { config_.precision = p; return *this; }
    InferConfigBuilder& setThreadCount(int n) { config_.thread_count = n; return *this; }
    InferConfigBuilder& setDevice(InferConfig::Device d) { config_.device = d; return *this; }
    InferConfigBuilder& setBatchSize(size_t b) { config_.batch_size = b; return *this; }
    InferConfigBuilder& enableCache(bool e) { config_.enable_cache = e; return *this; }
    InferConfigBuilder& setTimeout(int ms) { config_.timeout_ms = ms; return *this; }
    InferConfig build() {
        // 参数校验
        if (config_.precision == InferConfig::Precision::INT8 && config_.device == InferConfig::Device::CPU) {
            std::cerr << "Warning: INT8 on CPU may not be optimized\n";
        }
        return config_;
    }
private:
    InferConfig config_;
};
int main() {
    auto config = InferConfigBuilder()
        .setDevice(InferConfig::Device::GPU)
        .setPrecision(InferConfig::Precision::FP16)
        .setThreadCount(8)
        .setBatchSize(16)
        .enableCache(true)
        .build();
    config.print();
    return 0;
}
8.2.3 单例(Singleton)

单例在 AI 推理 SDK 中常用于全局唯一的组件:设备管理器(管理 GPU 设备的初始化和显存分配)、日志器(全局统一的日志输出)、算子注册表(全局唯一的算子注册中心)。

单例的线程安全实现有几种方式:Meyers Singleton(C++11 后局部静态变量初始化是线程安全的,最简单最推荐)、std::call_once(显式控制一次性初始化)、atomic 双检锁(DCLP,需要正确使用 memory_order_acquire/release,容易出错,不推荐手写)。

单例的危害也需要警惕:它引入了全局状态,使代码的依赖关系不透明(任何代码都可以随时访问单例,难以追踪谁在什么时候修改了它);它使单元测试变得困难(测试之间的全局状态会互相污染);它隐藏了类的真实依赖(构造函数不声明依赖,而是在内部直接访问单例)。替代方案是依赖注入(Dependency Injection):在类的构造函数中显式传入依赖对象,由上层组装对象关系。这样依赖关系清晰,便于测试和替换。

8.2.4 原型(Prototype)

原型模式通过克隆已有对象来创建新对象,在 AI 推理中适用于模型配置和张量模板的复制。比如一个模型有默认的推理配置,每次推理时需要基于默认配置创建一个副本,然后根据请求修改其中的批处理大小或超时时间。如果配置对象很复杂(包含嵌套的子配置、回调函数等),用构造函数重新构建很麻烦,而克隆已有对象更高效。C++ 中可以通过虚函数clone()实现原型模式,返回std::unique_ptr<Base>

8.3 结构型模式

8.3.1 适配器(Adapter)

适配器是 AI 推理 SDK 中最常用的结构型模式。不同的推理后端有完全不同的 API:TensorRT 用nvinfer1::IExecutionContext,ONNX Runtime 用Ort::Session,OpenVINO 用ov::InferRequest。如果 SDK 的上层代码直接调用这些 API,就会被特定后端绑定,无法灵活切换。

适配器模式将每个后端的 API 封装到一个统一的IInferenceBackend接口后面,每个后端提供一个适配器类,在内部将统一接口的调用翻译为对应后端的 API 调用。上层代码只依赖IInferenceBackend接口,完全不感知后端差异。新增后端时只需新增一个适配器类,不需要修改上层代码。

可编译代码示例:

复制代码
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// 统一接口
class IInferenceBackend {
public:
    virtual ~IInferenceBackend() = default;
    virtual void load_model(const std::string& path) = 0;
    virtual std::vector<float> infer(const std::vector<float>& input) = 0;
};
// 模拟TensorRT的API(第三方库,不可修改)
class TensorRTContext {
public:
    void deserialize(const std::string& engine_path) {
        std::cout << "[TensorRT] Deserializing engine: " << engine_path << "\n";
    }
    void enqueue(const float* input, float* output, int batch) {
        std::cout << "[TensorRT] Enqueue inference, batch=" << batch << "\n";
        output[0] = input[0] * 2.0f;  // 模拟
    }
};
// 模拟ONNX Runtime的API
class OrtSession {
public:
    void init(const std::string& model_path) {
        std::cout << "[ONNX Runtime] Initializing session: " << model_path << "\n";
    }
    void run(const float* input, float* output, size_t size) {
        std::cout << "[ONNX Runtime] Running inference, size=" << size << "\n";
        output[0] = input[0] * 3.0f;
    }
};
// TensorRT适配器
class TensorRTAdapter : public IInferenceBackend {
public:
    void load_model(const std::string& path) override {
        trt_ctx_.deserialize(path);
    }
    std::vector<float> infer(const std::vector<float>& input) override {
        std::vector<float> output(1);
        trt_ctx_.enqueue(input.data(), output.data(), 1);
        return output;
    }
private:
    TensorRTContext trt_ctx_;
};
// ONNX Runtime适配器
class ONNXRuntimeAdapter : public IInferenceBackend {
public:
    void load_model(const std::string& path) override {
        ort_session_.init(path);
    }
    std::vector<float> infer(const std::vector<float>& input) override {
        std::vector<float> output(1);
        ort_session_.run(input.data(), output.data(), input.size());
        return output;
    }
private:
    OrtSession ort_session_;
};
int main() {
    std::unique_ptr<IInferenceBackend> backend = std::make_unique<TensorRTAdapter>();
    backend->load_model("model.engine");
    auto result = backend->infer({1.0f});
    std::cout << "Result: " << result[0] << "\n";
    backend = std::make_unique<ONNXRuntimeAdapter>();
    backend->load_model("model.onnx");
    result = backend->infer({1.0f});
    std::cout << "Result: " << result[0] << "\n";
    return 0;
}
8.3.2 装饰器(Decorator)

装饰器模式允许在不修改核心类的情况下,动态地为对象添加功能。在 AI 推理服务中,一次推理请求可能需要日志记录、耗时统计、失败重试、结果缓存等横切关注点。如果将这些逻辑全部写在核心推理函数中,会导致函数臃肿且职责不清。装饰器模式将每个横切关注点封装为一个装饰器类,装饰器实现与核心类相同的接口,并持有一个被装饰对象的指针。调用时,装饰器在转发调用给被装饰对象的前后添加自己的逻辑。多个装饰器可以层层嵌套,形成装饰器链。

可编译代码示例:

复制代码
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <chrono>
#include <unordered_map>
// 组件接口
class IInferService {
public:
    virtual ~IInferService() = default;
    virtual std::vector<float> infer(const std::string& model, const std::vector<float>& input) = 0;
};
// 核心组件
class CoreInferService : public IInferService {
public:
    std::vector<float> infer(const std::string& model, const std::vector<float>& input) override {
        std::cout << "[Core] Executing inference for " << model << "\n";
        return {input[0] * 2.0f};
    }
};
// 装饰器基类
class InferDecorator : public IInferService {
public:
    explicit InferDecorator(std::shared_ptr<IInferService> inner) : inner_(std::move(inner)) {}
protected:
    std::shared_ptr<IInferService> inner_;
};
// 日志装饰器
class LoggingDecorator : public InferDecorator {
public:
    using InferDecorator::InferDecorator;
    std::vector<float> infer(const std::string& model, const std::vector<float>& input) override {
        std::cout << "[Log] Starting inference, model=" << model << ", input_size=" << input.size() << "\n";
        auto result = inner_->infer(model, input);
        std::cout << "[Log] Inference completed, output_size=" << result.size() << "\n";
        return result;
    }
};
// 计时装饰器
class TimingDecorator : public InferDecorator {
public:
    using InferDecorator::InferDecorator;
    std::vector<float> infer(const std::string& model, const std::vector<float>& input) override {
        auto start = std::chrono::high_resolution_clock::now();
        auto result = inner_->infer(model, input);
        auto end = std::chrono::high_resolution_clock::now();
        double ms = std::chrono::duration<double, std::milli>(end - start).count();
        std::cout << "[Timing] Inference took " << ms << " ms\n";
        return result;
    }
};
// 缓存装饰器
class CacheDecorator : public InferDecorator {
public:
    using InferDecorator::InferDecorator;
    std::vector<float> infer(const std::string& model, const std::vector<float>& input) override {
        std::string key = model + ":" + std::to_string(input[0]);
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            std::cout << "[Cache] Cache hit for key=" << key << "\n";
            return it->second;
        }
        auto result = inner_->infer(model, input);
        cache_[key] = result;
        return result;
    }
private:
    std::unordered_map<std::string, std::vector<float>> cache_;
};
int main() {
    auto core = std::make_shared<CoreInferService>();
    auto logged = std::make_shared<LoggingDecorator>(core);
    auto timed = std::make_shared<TimingDecorator>(logged);
    auto cached = std::make_shared<CacheDecorator>(timed);
    std::cout << "=== First call (cache miss) ===\n";
    cached->infer("resnet50", {1.0f});
    std::cout << "\n=== Second call (cache hit) ===\n";
    cached->infer("resnet50", {1.0f});
    return 0;
}
8.3.3 代理(Proxy)

代理模式与装饰器结构相同,但意图不同:装饰器是为了增加行为,代理是为了控制访问。在 AI 推理中常见的代理有三种:远程代理(本地对象代表远程推理服务,通过 gRPC/HTTP 调用远端,本地暴露相同的接口,客户端不感知网络通信)、虚拟代理 / 延迟加载代理(模型很大,加载耗时,代理在第一次推理时才真正加载模型,之前返回占位或快速路径)、保护代理(在推理前检查权限、限流、配额,控制对推理资源的访问)。

8.3.4 外观(Facade)

推理 SDK 的内部系统很复杂:模型加载器负责解析模型格式、内存管理器负责设备内存的分配和池化、后端调度器负责将算子分配到不同的计算设备、配置管理器负责全局和局部配置的合并。如果让 SDK 用户直接面对这些子系统,学习成本极高且容易出错。外观模式提供一个顶层的InferenceEngine类,内部封装所有子系统的交互,对外暴露简洁的接口(loadModelinferrelease)。用户只需要与外观类交互,不需要了解内部的复杂协作。

8.3.5 桥接(Bridge)

桥接模式将抽象部分与实现部分分离,使两者可以独立变化。在推理 SDK 中,InferRequest是抽象层,代表一次推理请求的概念(包含输入、输出、配置);BackendExecutor是实现层,负责真正执行推理(不同后端有不同的执行方式)。如果用继承来组合(CPUInferRequest、GPUInferRequest、AsyncCPUInferRequest...),类的数量会爆炸。桥接模式让 InferRequest 持有一个 BackendExecutor 的指针,请求类型和后端类型可以独立扩展,组合时通过指针关联而非继承。

8.3.6 组合(Composite)

计算图是 AI 推理的核心数据结构,由节点(算子)和边(张量)组成。计算图中可能包含子图(比如一个循环神经网络的展开单元、一个被融合的算子组),子图本身也是一种节点。组合模式将叶子节点(单个算子)和组合节点(子图)统一到同一个IGraphNode接口下,客户端可以用一致的方式处理单个算子和整个子图 ------ 比如遍历计算图时,不需要区分当前节点是单个算子还是子图,统一调用accept(visitor)execute()即可。

8.3.7 享元(Flyweight)

AI 推理中存在大量小对象:张量描述符(形状、数据类型、布局)、算子元信息(名称、属性 schema、输入输出数量)。如果每个张量都独立创建一个描述符对象,内存开销会很大 ------ 尤其是在处理大量小张量的场景(如注意力机制中的大量 query/key/value 向量)。享元模式将可共享的内部状态(如张量的形状和数据类型)提取为享元对象,通过享元工厂统一管理和复用,外部状态(如张量的数据指针、设备)单独存储。这样,大量形状相同的张量共享同一个描述符享元,显著减少内存占用。

8.4 行为型模式

8.4.1 策略(Strategy)

策略模式定义一系列可互换的算法,封装在独立的类中,使算法可以在运行时切换。在 AI 推理中,策略模式应用广泛:推理执行策略(同步执行、异步回调、流水线并行)、内存分配策略(池分配器复用内存、系统分配器直接 malloc/free、显存分配器调用 cudaMalloc)、精度校准策略(熵校准、最小 MSE 校准、百分位校准)。

可编译代码示例:内存分配策略

复制代码
#include <iostream>
#include <memory>
#include <vector>
#include <unordered_map>
// 策略接口
class IMemoryAllocator {
public:
    virtual ~IMemoryAllocator() = default;
    virtual void* allocate(size_t bytes) = 0;
    virtual void deallocate(void* ptr, size_t bytes) = 0;
    virtual std::string name() const = 0;
};
// 系统分配策略
class SystemAllocator : public IMemoryAllocator {
public:
    void* allocate(size_t bytes) override {
        std::cout << "[System] malloc " << bytes << " bytes\n";
        return std::malloc(bytes);
    }
    void deallocate(void* ptr, size_t bytes) override {
        std::cout << "[System] free " << bytes << " bytes\n";
        std::free(ptr);
    }
    std::string name() const override { return "SystemAllocator"; }
};
// 内存池分配策略
class PoolAllocator : public IMemoryAllocator {
public:
    void* allocate(size_t bytes) override {
        // 先在池中查找可复用的块
        auto it = free_blocks_.find(bytes);
        if (it != free_blocks_.end() && !it->second.empty()) {
            void* ptr = it->second.back();
            it->second.pop_back();
            std::cout << "[Pool] Reuse " << bytes << " bytes from pool\n";
            return ptr;
        }
        void* ptr = std::malloc(bytes);
        std::cout << "[Pool] Allocate new " << bytes << " bytes\n";
        return ptr;
    }
    void deallocate(void* ptr, size_t bytes) override {
        free_blocks_[bytes].push_back(ptr);
        std::cout << "[Pool] Return " << bytes << " bytes to pool\n";
    }
    std::string name() const override { return "PoolAllocator"; }
private:
    std::unordered_map<size_t, std::vector<void*>> free_blocks_;
};
// 上下文:使用策略的类
class TensorManager {
public:
    explicit TensorManager(std::unique_ptr<IMemoryAllocator> allocator)
        : allocator_(std::move(allocator)) {}
    void set_allocator(std::unique_ptr<IMemoryAllocator> allocator) {
        allocator_ = std::move(allocator);
    }
    void* create_tensor(size_t bytes) {
        return allocator_->allocate(bytes);
    }
    void destroy_tensor(void* ptr, size_t bytes) {
        allocator_->deallocate(ptr, bytes);
    }
private:
    std::unique_ptr<IMemoryAllocator> allocator_;
};
int main() {
    TensorManager manager(std::make_unique<PoolAllocator>());
    void* t1 = manager.create_tensor(1024);
    manager.destroy_tensor(t1, 1024);
    void* t2 = manager.create_tensor(1024);  // 复用池中的内存
    manager.destroy_tensor(t2, 1024);
    // 运行时切换策略
    manager.set_allocator(std::make_unique<SystemAllocator>());
    void* t3 = manager.create_tensor(2048);
    manager.destroy_tensor(t3, 2048);
    return 0;
}

可编译代码示例:推理执行策略(同步 / 异步 / 流水线)

复制代码
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <chrono>
#include <future>
// 策略接口:推理执行策略
class IExecutionStrategy {
public:
    virtual ~IExecutionStrategy() = default;
    virtual void execute(const std::string& model) = 0;
    virtual std::string name() const = 0;
};
// 同步执行策略
class SyncExecution : public IExecutionStrategy {
public:
    void execute(const std::string& model) override {
        std::cout << "[Sync] Running " << model << " synchronously\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟推理
        std::cout << "[Sync] Done\n";
    }
    std::string name() const override { return "SyncExecution"; }
};
// 异步回调执行策略
class AsyncExecution : public IExecutionStrategy {
public:
    void execute(const std::string& model) override {
        std::cout << "[Async] Launching " << model << " asynchronously\n";
        std::async(std::launch::async, [model] {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟推理
            std::cout << "[Async] Callback: " << model << " completed\n";
        });
    }
    std::string name() const override { return "AsyncExecution"; }
};
// 流水线并行执行策略
class PipelineExecution : public IExecutionStrategy {
public:
    void execute(const std::string& model) override {
        std::cout << "[Pipeline] Running " << model << " in pipeline stages\n";
        std::thread stage1([] { std::this_thread::sleep_for(std::chrono::milliseconds(30)); std::cout << "[Pipeline] Stage1: preprocess\n"; });
        std::thread stage2([] { std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::cout << "[Pipeline] Stage2: inference\n"; });
        std::thread stage3([] { std::this_thread::sleep_for(std::chrono::milliseconds(20)); std::cout << "[Pipeline] Stage3: postprocess\n"; });
        stage1.join(); stage2.join(); stage3.join();
        std::cout << "[Pipeline] All stages done\n";
    }
    std::string name() const override { return "PipelineExecution"; }
};
// 上下文:推理引擎,运行时切换策略
class InferenceEngine {
public:
    explicit InferenceEngine(std::unique_ptr<IExecutionStrategy> strategy)
        : strategy_(std::move(strategy)) {}
    void set_strategy(std::unique_ptr<IExecutionStrategy> strategy) {
        strategy_ = std::move(strategy);
    }
    void run(const std::string& model) {
        std::cout << "Using strategy: " << strategy_->name() << "\n";
        strategy_->execute(model);
    }
private:
    std::unique_ptr<IExecutionStrategy> strategy_;
};
int main() {
    InferenceEngine engine(std::make_unique<SyncExecution>());
    engine.run("resnet50");
    std::cout << "\n";
    // 运行时切换为异步策略
    engine.set_strategy(std::make_unique<AsyncExecution>());
    engine.run("resnet50");
    std::cout << "\n";
    // 运行时切换为流水线策略
    engine.set_strategy(std::make_unique<PipelineExecution>());
    engine.run("resnet50");
    return 0;
}
/* 运行结果示例:
Using strategy: SyncExecution
[Sync] Running resnet50 synchronously
[Sync] Done

Using strategy: AsyncExecution
[Async] Launching resnet50 asynchronously
[Async] Callback: resnet50 completed

Using strategy: PipelineExecution
[Pipeline] Running resnet50 in pipeline stages
[Pipeline] Stage1: preprocess
[Pipeline] Stage2: inference
[Pipeline] Stage3: postprocess
[Pipeline] All stages done
*/
8.4.2 观察者(Observer)/ 发布订阅

观察者模式定义了对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。在 AI 推理 SDK 中,观察者模式用于事件通知:推理开始事件、推理完成事件、推理错误事件、进度更新事件。外部系统(如监控系统、日志系统、UI 界面)可以注册为观察者,在事件发生时收到通知,而不需要推理引擎主动调用这些外部系统的接口 ------ 这实现了推理引擎与外部系统的解耦。

在 Agent 工具运行时中,观察者模式更为重要:工具执行前事件(用于日志和鉴权)、工具执行后事件(用于结果记录和指标统计)、LLM 调用事件(用于 token 计数和成本监控)、错误事件(用于告警和重试触发)。一个事件总线(EventBus)统一管理事件的发布和订阅,各组件通过事件总线通信而非直接调用。

可编译代码示例:

复制代码
#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include <memory>
// 事件类型
struct InferEvent {
    enum class Type { START, COMPLETE, ERROR, PROGRESS };
    Type type;
    std::string model_name;
    std::string message;
    double progress;  // 0.0 - 1.0
};
// 观察者接口
class IInferObserver {
public:
    virtual ~IInferObserver() = default;
    virtual void on_event(const InferEvent& event) = 0;
};
// 具体观察者:日志记录器
class LoggingObserver : public IInferObserver {
public:
    void on_event(const InferEvent& event) override {
        std::cout << "[LOG] ";
        switch (event.type) {
            case InferEvent::Type::START: std::cout << "START"; break;
            case InferEvent::Type::COMPLETE: std::cout << "COMPLETE"; break;
            case InferEvent::Type::ERROR: std::cout << "ERROR"; break;
            case InferEvent::Type::PROGRESS: std::cout << "PROGRESS"; break;
        }
        std::cout << " model=" << event.model_name << " msg=" << event.message << "\n";
    }
};
// 具体观察者:指标收集器
class MetricsObserver : public IInferObserver {
public:
    void on_event(const InferEvent& event) override {
        if (event.type == InferEvent::Type::COMPLETE) {
            ++complete_count_;
            std::cout << "[METRICS] Total completed: " << complete_count_ << "\n";
        } else if (event.type == InferEvent::Type::ERROR) {
            ++error_count_;
            std::cout << "[METRICS] Total errors: " << error_count_ << "\n";
        }
    }
private:
    int complete_count_ = 0;
    int error_count_ = 0;
};
// 被观察者:推理引擎
class InferEngine {
public:
    void subscribe(std::shared_ptr<IInferObserver> observer) {
        observers_.push_back(std::move(observer));
    }
    void run_inference(const std::string& model) {
        publish({InferEvent::Type::START, model, "Inference started", 0.0});
        // 模拟推理过程
        publish({InferEvent::Type::PROGRESS, model, "Processing", 0.5});
        publish({InferEvent::Type::COMPLETE, model, "Inference done", 1.0});
    }
private:
    void publish(const InferEvent& event) {
        for (auto& obs : observers_) obs->on_event(event);
    }
    std::vector<std::shared_ptr<IInferObserver>> observers_;
};
int main() {
    InferEngine engine;
    engine.subscribe(std::make_shared<LoggingObserver>());
    engine.subscribe(std::make_shared<MetricsObserver>());
    engine.run_inference("bert-base");
    return 0;
}

上面的示例展示了推理开始、进度和完成事件的通知机制。下面补充一个更贴近真实推理场景的版本,覆盖推理开始、结束、失败三类事件,并演示观察者如何通过事件总线解耦:

复制代码
#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include <memory>
#include <unordered_map>

// ========== 1. 事件定义:推理开始 / 结束 / 失败 ==========
struct InferenceEvent {
    enum class Type { STARTED, FINISHED, FAILED };
    Type type;
    std::string request_id;   // 请求唯一标识
    std::string model_name;   // 模型名称
    std::string detail;       // 附加信息(如错误原因、耗时)
};

// ========== 2. 观察者接口:所有订阅方实现该接口 ==========
class IInferenceObserver {
public:
    virtual ~IInferenceObserver() = default;
    virtual void on_inference_event(const InferenceEvent& event) = 0;
};

// ========== 3. 具体观察者:监控系统(关注开始/结束/失败) ==========
class MonitoringObserver : public IInferenceObserver {
public:
    void on_inference_event(const InferenceEvent& event) override {
        switch (event.type) {
            case InferenceEvent::Type::STARTED:
                std::cout << "[Monitor] Request " << event.request_id
                          << " started, model=" << event.model_name << "\n";
                break;
            case InferenceEvent::Type::FINISHED:
                std::cout << "[Monitor] Request " << event.request_id
                          << " finished, detail=" << event.detail << "\n";
                break;
            case InferenceEvent::Type::FAILED:
                std::cout << "[Monitor] Request " << event.request_id
                          << " FAILED, reason=" << event.detail << "\n";
                break;
        }
    }
};

// ========== 4. 具体观察者:日志系统(记录所有事件) ==========
class LoggingObserver : public IInferenceObserver {
public:
    void on_inference_event(const InferenceEvent& event) override {
        std::cout << "[Log] ";
        switch (event.type) {
            case InferenceEvent::Type::STARTED:  std::cout << "STARTED";  break;
            case InferenceEvent::Type::FINISHED: std::cout << "FINISHED"; break;
            case InferenceEvent::Type::FAILED:   std::cout << "FAILED";   break;
        }
        std::cout << " req=" << event.request_id
                  << " model=" << event.model_name
                  << " detail=" << event.detail << "\n";
    }
};

// ========== 5. 事件总线:统一管理订阅与发布(观察者核心) ==========
class EventBus {
public:
    // 订阅:观察者注册到总线,按事件类型分类存储
    void subscribe(InferenceEvent::Type type, std::shared_ptr<IInferenceObserver> observer) {
        observers_[type].push_back(std::move(observer));
    }
    // 发布:向订阅了该事件类型的所有观察者广播
    void publish(const InferenceEvent& event) {
        auto it = observers_.find(event.type);
        if (it == observers_.end()) return;  // 无订阅者则直接返回
        for (auto& obs : it->second) {
            obs->on_inference_event(event);
        }
    }
private:
    // 关键点:按事件类型分组,观察者只收到自己关心的事件
    std::unordered_map<InferenceEvent::Type, std::vector<std::shared_ptr<IInferenceObserver>>> observers_;
};

// ========== 6. 被观察者:推理引擎(只依赖事件总线,不感知具体观察者) ==========
class InferenceEngine {
public:
    explicit InferenceEngine(EventBus& bus) : bus_(bus) {}

    // 模拟一次推理:开始 -> 成功 或 开始 -> 失败
    void run(const std::string& request_id, const std::string& model, bool simulate_failure) {
        // 推理开始事件
        bus_.publish({InferenceEvent::Type::STARTED, request_id, model, "inference begins"});

        if (simulate_failure) {
            // 推理失败事件
            bus_.publish({InferenceEvent::Type::FAILED, request_id, model, "CUDA out of memory"});
            return;
        }
        // 推理结束事件
        bus_.publish({InferenceEvent::Type::FINISHED, request_id, model, "latency=12.3ms"});
    }
private:
    EventBus& bus_;  // 关键点:引擎只持有总线引用,与观察者完全解耦
};

// ========== 7. 使用示例 ==========
int main() {
    EventBus bus;

    // 注册观察者:监控系统关注所有事件,日志系统也关注所有事件
    auto monitor = std::make_shared<MonitoringObserver>();
    auto logger  = std::make_shared<LoggingObserver>();
    bus.subscribe(InferenceEvent::Type::STARTED,  monitor);
    bus.subscribe(InferenceEvent::Type::FINISHED, monitor);
    bus.subscribe(InferenceEvent::Type::FAILED,   monitor);
    bus.subscribe(InferenceEvent::Type::STARTED,  logger);
    bus.subscribe(InferenceEvent::Type::FINISHED, logger);
    bus.subscribe(InferenceEvent::Type::FAILED,   logger);

    InferenceEngine engine(bus);

    std::cout << "--- 成功推理 ---\n";
    engine.run("req-001", "bert-base", /*simulate_failure=*/false);

    std::cout << "\n--- 失败推理 ---\n";
    engine.run("req-002", "gpt-3.5", /*simulate_failure=*/true);

    return 0;
}
/* 运行结果示例:
--- 成功推理 ---
[Monitor] Request req-001 started, model=bert-base
[Log] STARTED req=req-001 model=bert-base detail=inference begins
[Monitor] Request req-001 finished, detail=latency=12.3ms
[Log] FINISHED req=req-001 model=bert-base detail=latency=12.3ms

--- 失败推理 ---
[Monitor] Request req-002 started, model=gpt-3.5
[Log] STARTED req=req-002 model=gpt-3.5 detail=inference begins
[Monitor] Request req-002 FAILED, reason=CUDA out of memory
[Log] FAILED req=req-002 model=gpt-3.5 detail=CUDA out of memory
*/

关键点说明:

  • 事件类型化 :通过InferenceEvent::Type区分开始、结束、失败,观察者按类型订阅,只处理自己关心的事件。
  • 事件总线解耦 :推理引擎只依赖EventBus发布事件,不持有任何观察者指针,新增观察者无需修改引擎代码。
  • 生命周期安全 :观察者以std::shared_ptr注册,避免悬垂指针;实际工程中可结合std::weak_ptr或析构时主动注销,防止 use-after-free。
  • 失败通知:失败事件携带错误原因(如 CUDA 内存不足),便于监控告警和重试策略触发。
8.4.3 责任链(Chain of Responsibility)

责任链模式将请求的处理分为多个独立的环节,每个环节只负责自己的处理逻辑,然后将请求传递给下一个环节。环节可以动态添加、移除或重新排序,实现了请求处理流程的灵活配置。

在 AI 推理服务中,一次请求的处理流程通常是:鉴权(检查 API key 和权限)→限流(检查 QPS 配额)→预处理(图像解码、文本分词、数据归一化)→推理(核心计算)→后处理(结果解析、阈值过滤、格式转换)→缓存(将结果写入缓存供后续相同请求使用)。每个环节都是一个独立的处理器,通过责任链串联。

在 Agent 工具运行时中,工具调用链类似:工具选择(根据 LLM 输出选择工具)→参数校验(检查工具参数是否完整合法)→执行(调用工具函数)→结果格式化(将工具返回值格式化为 LLM 可理解的文本)。

可编译代码示例:

复制代码
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// 请求上下文
struct InferRequest {
    std::string api_key;
    std::string model;
    std::vector<float> input;
    std::vector<float> output;
    bool authorized = false;
    bool rate_limited = false;
    std::string error;
};
// 处理器接口
class IRequestHandler {
public:
    virtual ~IRequestHandler() = default;
    virtual void handle(InferRequest& req) = 0;
    void set_next(std::shared_ptr<IRequestHandler> next) { next_ = std::move(next); }
protected:
    void pass_to_next(InferRequest& req) {
        if (next_) next_->handle(req);
    }
    std::shared_ptr<IRequestHandler> next_;
};
// 鉴权处理器
class AuthHandler : public IRequestHandler {
public:
    void handle(InferRequest& req) override {
        if (req.api_key == "valid_key") {
            req.authorized = true;
            std::cout << "[Auth] Authorized\n";
            pass_to_next(req);
        } else {
            req.error = "Unauthorized: invalid API key";
            std::cout << "[Auth] Rejected: " << req.error << "\n";
        }
    }
};
// 限流处理器
class RateLimitHandler : public IRequestHandler {
public:
    void handle(InferRequest& req) override {
        if (request_count_ < max_rps_) {
            ++request_count_;
            std::cout << "[RateLimit] Passed (count=" << request_count_ << ")\n";
            pass_to_next(req);
        } else {
            req.error = "Rate limit exceeded";
            std::cout << "[RateLimit] Rejected\n";
        }
    }
private:
    int request_count_ = 0;
    int max_rps_ = 10;
};
// 预处理处理器
class PreprocessHandler : public IRequestHandler {
public:
    void handle(InferRequest& req) override {
        std::cout << "[Preprocess] Normalizing input\n";
        for (auto& v : req.input) v = v / 255.0f;  // 模拟归一化
        pass_to_next(req);
    }
};
// 推理处理器
class InferHandler : public IRequestHandler {
public:
    void handle(InferRequest& req) override {
        std::cout << "[Infer] Executing model " << req.model << "\n";
        req.output = {req.input[0] * 2.0f};
        pass_to_next(req);
    }
};
// 后处理处理器
class PostprocessHandler : public IRequestHandler {
public:
    void handle(InferRequest& req) override {
        std::cout << "[Postprocess] Formatting output\n";
        for (auto& v : req.output) v = v > 0.5f ? 1.0f : 0.0f;  // 阈值过滤
        pass_to_next(req);
    }
};
// 责任链构建器
class RequestPipeline {
public:
    void add_handler(std::shared_ptr<IRequestHandler> handler) {
        if (!head_) {
            head_ = handler;
            tail_ = handler;
        } else {
            tail_->set_next(handler);
            tail_ = handler;
        }
    }
    void process(InferRequest& req) {
        if (head_) head_->handle(req);
    }
private:
    std::shared_ptr<IRequestHandler> head_;
    std::shared_ptr<IRequestHandler> tail_;
};
int main() {
    RequestPipeline pipeline;
    pipeline.add_handler(std::make_shared<AuthHandler>());
    pipeline.add_handler(std::make_shared<RateLimitHandler>());
    pipeline.add_handler(std::make_shared<PreprocessHandler>());
    pipeline.add_handler(std::make_shared<InferHandler>());
    pipeline.add_handler(std::make_shared<PostprocessHandler>());
    InferRequest req{"valid_key", "resnet50", {128.0f}, {}, false, false, ""};
    pipeline.process(req);
    if (!req.error.empty()) std::cout << "Error: " << req.error << "\n";
    else std::cout << "Final output: " << req.output[0] << "\n";
    return 0;
}
8.4.4 模板方法(Template Method)

模板方法模式在基类中定义算法的骨架,将某些步骤延迟到子类中实现。不同后端的推理流程有相同的骨架:准备输入(将输入张量拷贝到设备内存、设置输入形状)→执行核函数(调用后端的算子实现)→后处理(将输出拷贝回主机内存、转换格式)。但每个步骤的具体实现因后端而异。模板方法在基类BackendBase中定义infer()方法,按顺序调用prepare_input()execute_kernel()postprocess()三个虚函数,具体后端(CPUBackend、GPUBackend)实现这三个步骤。这样保证了所有后端的推理流程一致,同时允许各步骤的实现差异化。

可编译代码示例:

复制代码
#include <iostream>
#include <vector>
#include <string>
// 模板方法基类
class BackendBase {
public:
    virtual ~BackendBase() = default;
    // 模板方法:定义推理流程骨架
    std::vector<float> infer(const std::vector<float>& input) {
        std::cout << "=== " << name() << " Inference Pipeline ===\n";
        auto device_input = prepare_input(input);
        auto device_output = execute_kernel(device_input);
        auto result = postprocess(device_output);
        std::cout << "=== Pipeline complete ===\n";
        return result;
    }
protected:
    // 步骤1:准备输入(子类可覆盖)
    virtual std::vector<float> prepare_input(const std::vector<float>& input) {
        std::cout << "[" << name() << "] Preparing input (copy to device)\n";
        return input;  // 默认实现:直接返回
    }
    // 步骤2:执行核函数(必须由子类实现)
    virtual std::vector<float> execute_kernel(const std::vector<float>& input) = 0;
    // 步骤3:后处理(子类可覆盖)
    virtual std::vector<float> postprocess(const std::vector<float>& output) {
        std::cout << "[" << name() << "] Postprocessing (copy back to host)\n";
        return output;
    }
    virtual std::string name() const = 0;
};
// CPU后端
class CPUBackend : public BackendBase {
protected:
    std::vector<float> execute_kernel(const std::vector<float>& input) override {
        std::cout << "[CPU] Executing kernel on CPU\n";
        std::vector<float> output(input.size());
        for (size_t i = 0; i < input.size(); ++i) output[i] = input[i] * 2.0f;
        return output;
    }
    std::string name() const override { return "CPU"; }
};
// GPU后端
class GPUBackend : public BackendBase {
protected:
    std::vector<float> prepare_input(const std::vector<float>& input) override {
        std::cout << "[GPU] Copying input to GPU memory (cudaMemcpy)\n";
        return input;
    }
    std::vector<float> execute_kernel(const std::vector<float>& input) override {
        std::cout << "[GPU] Launching CUDA kernel\n";
        std::vector<float> output(input.size());
        for (size_t i = 0; i < input.size(); ++i) output[i] = input[i] * 2.0f;
        return output;
    }
    std::vector<float> postprocess(const std::vector<float>& output) override {
        std::cout << "[GPU] Copying output back to host (cudaMemcpy)\n";
        return output;
    }
    std::string name() const override { return "GPU"; }
};
int main() {
    CPUBackend cpu;
    auto cpu_result = cpu.infer({1.0f, 2.0f, 3.0f});
    GPUBackend gpu;
    auto gpu_result = gpu.infer({1.0f, 2.0f, 3.0f});
    return 0;
}
8.4.5 迭代器(Iterator)

迭代器模式提供一种统一的方式遍历聚合对象中的元素,而不暴露其内部表示。在 AI 推理中,迭代器的应用包括:张量维度迭代(遍历张量的每个元素、每个通道、每个空间位置)、数据集批次迭代(遍历训练 / 推理数据集,每次返回一个批次的数据)、计算图节点遍历(深度优先或广度优先遍历计算图中的所有节点)。C++ 标准库的迭代器概念(begin/end、++、*、->)是迭代器模式的语言级实现,AI 框架中的张量迭代器通常遵循 STL 迭代器规范,可以与 STL 算法配合使用。

8.4.6 状态(State)

推理请求有明确的生命周期状态:CREATED(请求已创建,未开始处理)→LOADING(模型加载中,如果模型未缓存)→READY(模型已就绪,等待执行)→RUNNING(推理执行中)→COMPLETED(推理完成,结果可用)→ERROR(推理失败,包含错误信息)。状态模式将每个状态封装为一个类,状态类实现该状态下允许的行为和状态转换逻辑。请求对象持有一个当前状态的指针,行为调用委托给当前状态对象。当状态转换时,更换状态指针即可。这避免了在请求类中写大量的if (state == ...) else if (state == ...)分支,使状态转换逻辑清晰且易于扩展。

8.4.7 命令(Command)

命令模式将一个请求或操作封装为一个对象,从而可以用不同的请求对客户端进行参数化、排队请求、记录请求日志、支持撤销操作。在 Agent 工具运行时中,每个工具调用可以封装为一个命令对象:命令对象包含工具名称、参数、执行方法、撤销方法(如果工具支持)、重试策略。命令对象可以被排队执行(控制并发数)、被记录日志(审计工具调用历史)、被重试(网络类工具失败后自动重试)、被撤销(如文件操作类工具支持回滚)。

8.4.8 访问者(Visitor)

访问者模式允许在不修改元素类的前提下,为元素类添加新的操作。在 AI 推理的计算图优化中,计算图由各种算子节点组成(卷积、池化、全连接、激活等),优化 pass 需要对不同类型的节点做不同的处理:常量折叠 pass 需要识别常量节点并预计算,算子融合 pass 需要识别相邻的卷积 + 激活节点并合并,布局转换 pass 需要根据目标设备调整节点的数据布局。如果为每个 pass 都在节点类中添加虚函数,节点类会越来越臃肿,且新增 pass 需要修改所有节点类。访问者模式定义IGraphVisitor接口,每个 pass 是一个具体访问者,节点类提供accept(IGraphVisitor&)方法,在 accept 中调用访问者对应的visit(ConvolutionNode&)等方法。新增优化 pass 只需新增一个访问者类,不需要修改节点类。

8.4.9 中介者(Mediator)

中介者模式定义一个中介对象来封装一系列对象的交互,使各对象不需要显式地相互引用,从而降低耦合度。在 Agent 工具运行时中,工具之间可能需要协作(比如搜索工具找到信息后,需要调用摘要工具生成摘要,再调用格式化工具输出)。如果工具之间直接调用,会形成复杂的依赖网络,新增工具时需要修改所有相关工具。中介者模式引入一个ToolMediator,所有工具只与中介者通信,中介者负责协调工具之间的调用关系。工具不需要知道其他工具的存在,只需要向中介者发出请求,中介者决定调用哪个工具、如何传递结果。

8.5 RAII:C++ 特有的资源管理模式

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 最重要的设计模式,甚至可以说它是 C++ 语言的灵魂。RAII 的核心思想是:将资源的生命周期与对象的生命周期绑定 ------ 在构造函数中获取资源,在析构函数中释放资源。当对象离开作用域时,析构函数自动调用,资源自动释放,无论正常返回还是异常抛出。

在 AI 推理 SDK 中,RAII 无处不在:

  • 智能指针管理设备内存:std::unique_ptr<float, CudaDeleter>管理 cudaMalloc 分配的显存,自定义 deleter 在析构时调用 cudaFree。

  • std::lock_guard管理互斥锁:在作用域开始时加锁,离开时自动解锁,避免忘记解锁导致死锁,也避免异常导致锁未释放。

  • scope_exit管理通用清理:用 GSL 的finally或自己实现的 scope_exit,在作用域结束时执行指定的清理函数(如关闭文件描述符、释放临时缓冲区、恢复全局状态)。

  • 文件句柄管理:自定义FileHandle类,构造时打开文件,析构时关闭。

  • 模型句柄管理:TensorRT 的IExecutionContext等第三方句柄,封装为 RAII 类,析构时调用对应的 destroy 方法。

RAII 的价值不仅在于防止资源泄漏,更在于异常安全。在 C++ 中,异常可以在任何时候抛出,如果资源的获取和释放不是通过 RAII 管理,而是手动写acquire(); ...; release();,那么中间代码抛出异常时release()不会被调用,资源泄漏。RAII 保证了无论控制流如何离开作用域(正常 return、break、continue、异常),析构函数都会被调用。

8.6 Pimpl:编译防火墙与 ABI 稳定

Pimpl(Pointer to Implementation,指针实现)模式将类的实现细节隐藏在一个不透明的指针后面。类的头文件中只声明一个struct Impl;前向声明和一个std::unique_ptr<Impl> pimpl_;成员,所有的私有成员变量和私有方法都放在Impl结构体中,在源文件中定义。

Pimpl 的价值在于:

  • 编译防火墙:头文件中不再包含实现所需的其他头文件(如第三方库的头文件、内部子系统的头文件),修改实现细节不会导致包含该头文件的其他文件重新编译,大幅缩短编译时间。

  • ABI 稳定:类的大小和布局不随实现细节变化(始终是一个指针的大小),因此可以在不重新编译客户端代码的情况下更新库的实现。这对于二进制分发的 SDK 至关重要。

  • 减少头文件依赖:客户端只需要看到类的公共接口,不需要知道内部用了哪些第三方库和数据结构。

在 AI 推理 SDK 中,Pimpl 是标准实践。顶层的InferenceEngine类的头文件只包含公共接口,内部的模型加载器、内存管理器、后端调度器等都隐藏在Impl中。这样,SDK 用户的代码只依赖稳定的公共接口,SDK 内部的重构和优化不会影响用户。

8.7 类型擦除:统一不同签名的工具函数

类型擦除(Type Erasure)是一种 C++ 技术,用于在保留值语义的同时擦除类型信息,使不同类型的对象可以用统一的方式处理。标准库中的std::functionstd::anystd::variant都是类型擦除的应用。

在 Agent 工具运行时中,不同的工具函数有不同的签名:有的工具接受字符串参数返回字符串,有的接受 JSON 对象返回结构化数据,有的不需要参数。如果为每种签名定义一个独立的工具接口,运行时需要处理多种接口类型,非常繁琐。类型擦除可以将所有工具函数统一为一个Tool类型,内部用std::function或自定义的类型擦除容器存储不同签名的函数,对外暴露统一的execute(const ToolArgs&) -> ToolResult接口。

std::function是最常用的类型擦除工具,它可以存储任何可调用对象(函数指针、lambda、仿函数),只要签名匹配。但std::function有动态分配的开销(当可调用对象较大时),且不支持 noexcept。在性能敏感的场景中,可以使用function_ref(C++26 标准,或自己实现)------ 它是一个非拥有的引用,不做动态分配,开销极小,但要求被引用的可调用对象生命周期足够长。

std::any可以存储任何可拷贝构造的类型,通过any_cast<T>提取具体值。在 Agent 运行时中,工具的参数和返回值可以用std::any传递,实现完全的类型无关性,但需要调用方知道实际类型才能安全提取。

8.8 Agent 工具运行时场景专题

Agent 工具运行时是设计模式的集大成者,它综合运用了工厂、责任链、命令、观察者、状态、中介者等多种模式。下面通过一个简化的AgentToolRuntime实现来展示这些模式如何协同工作。

可编译代码示例:

复制代码
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <functional>
#include <any>
// ========== 工具接口与注册表(工厂模式) ==========
struct ToolContext {
    std::unordered_map<std::string, std::string> params;
    std::string result;
    std::string error;
};
class ITool {
public:
    virtual ~ITool() = default;
    virtual std::string name() const = 0;
    virtual std::string description() const = 0;
    virtual void execute(ToolContext& ctx) = 0;
};
class SearchTool : public ITool {
public:
    std::string name() const override { return "search"; }
    std::string description() const override { return "Search the web for information"; }
    void execute(ToolContext& ctx) override {
        auto it = ctx.params.find("query");
        if (it == ctx.params.end()) { ctx.error = "Missing 'query' parameter"; return; }
        ctx.result = "Search results for: " + it->second + " (simulated)";
    }
};
class CalculatorTool : public ITool {
public:
    std::string name() const override { return "calculator"; }
    std::string description() const override { return "Perform mathematical calculations"; }
    void execute(ToolContext& ctx) override {
        auto it = ctx.params.find("expression");
        if (it == ctx.params.end()) { ctx.error = "Missing 'expression' parameter"; return; }
        ctx.result = "Result of " + it->second + " = 42 (simulated)";
    }
};
// 工具注册表(单例+工厂)
class ToolRegistry {
public:
    static ToolRegistry& instance() {
        static ToolRegistry inst;
        return inst;
    }
    void register_tool(std::unique_ptr<ITool> tool) {
        tools_[tool->name()] = std::move(tool);
    }
    ITool* get_tool(const std::string& name) {
        auto it = tools_.find(name);
        return it == tools_.end() ? nullptr : it->second.get();
    }
    std::vector<std::string> list_tools() const {
        std::vector<std::string> names;
        for (auto& [name, tool] : tools_) names.push_back(name);
        return names;
    }
private:
    std::unordered_map<std::string, std::unique_ptr<ITool>> tools_;
};
// ========== 责任链:工具调用处理器 ==========
class IToolHandler {
public:
    virtual ~IToolHandler() = default;
    virtual void handle(ToolContext& ctx, ITool* tool) = 0;
    void set_next(std::shared_ptr<IToolHandler> next) { next_ = std::move(next); }
protected:
    void pass_next(ToolContext& ctx, ITool* tool) { if (next_) next_->handle(ctx, tool); }
    std::shared_ptr<IToolHandler> next_;
};
class ValidationHandler : public IToolHandler {
public:
    void handle(ToolContext& ctx, ITool* tool) override {
        std::cout << "[Validate] Checking parameters for " << tool->name() << "\n";
        pass_next(ctx, tool);
    }
};
class LoggingHandler : public IToolHandler {
public:
    void handle(ToolContext& ctx, ITool* tool) override {
        std::cout << "[Log] Executing tool: " << tool->name() << "\n";
        pass_next(ctx, tool);
        if (!ctx.error.empty()) std::cout << "[Log] Tool failed: " << ctx.error << "\n";
        else std::cout << "[Log] Tool succeeded\n";
    }
};
class ExecutionHandler : public IToolHandler {
public:
    void handle(ToolContext& ctx, ITool* tool) override {
        tool->execute(ctx);
        pass_next(ctx, tool);
    }
};
// ========== 观察者:工具事件 ==========
struct ToolEvent {
    enum class Type { BEFORE_EXECUTE, AFTER_EXECUTE, ERROR };
    Type type;
    std::string tool_name;
};
class IToolObserver {
public:
    virtual ~IToolObserver() = default;
    virtual void on_tool_event(const ToolEvent& event) = 0;
};
class MetricsObserver : public IToolObserver {
public:
    void on_tool_event(const ToolEvent& event) override {
        if (event.type == ToolEvent::Type::AFTER_EXECUTE)
            std::cout << "[Metrics] Tool " << event.tool_name << " completed\n";
    }
};
// ========== Agent运行时(外观+中介者) ==========
class AgentToolRuntime {
public:
    AgentToolRuntime() {
        // 构建责任链
        auto validation = std::make_shared<ValidationHandler>();
        auto logging = std::make_shared<LoggingHandler>();
        auto execution = std::make_shared<ExecutionHandler>();
        validation->set_next(logging);
        logging->set_next(execution);
        pipeline_head_ = validation;
    }
    void subscribe(std::shared_ptr<IToolObserver> obs) { observers_.push_back(std::move(obs)); }
    std::string call_tool(const std::string& name,
                          const std::unordered_map<std::string, std::string>& params) {
        ITool* tool = ToolRegistry::instance().get_tool(name);
        if (!tool) return "Error: Tool not found: " + name;
        ToolContext ctx;
        ctx.params = params;
        publish({ToolEvent::Type::BEFORE_EXECUTE, name});
        pipeline_head_->handle(ctx, tool);
        if (!ctx.error.empty()) {
            publish({ToolEvent::Type::ERROR, name});
            return "Error: " + ctx.error;
        }
        publish({ToolEvent::Type::AFTER_EXECUTE, name});
        return ctx.result;
    }
private:
    void publish(const ToolEvent& event) {
        for (auto& obs : observers_) obs->on_tool_event(event);
    }
    std::shared_ptr<IToolHandler> pipeline_head_;
    std::vector<std::shared_ptr<IToolObserver>> observers_;
};
int main() {
    // 注册工具
    ToolRegistry::instance().register_tool(std::make_unique<SearchTool>());
    ToolRegistry::instance().register_tool(std::make_unique<CalculatorTool>());
    // 创建运行时
    AgentToolRuntime runtime;
    runtime.subscribe(std::make_shared<MetricsObserver>());
    // 调用工具
    std::cout << runtime.call_tool("search", {{"query", "C++ design patterns"}}) << "\n";
    std::cout << "\n";
    std::cout << runtime.call_tool("calculator", {{"expression", "2+2"}}) << "\n";
    return 0;
}

这个简化的 Agent 运行时综合了多种设计模式:工具注册表用了单例 + 工厂,调用流程用了责任链,事件通知用了观察者,运行时类本身是外观和中介者。每种模式解决一个特定的问题,组合起来形成一个可扩展、可维护的系统架构。

8.9 设计模式组合使用

实际系统中的设计模式从来不是孤立使用的,而是根据需求组合在一起。一个典型的可扩展推理流水线的模式组合是:工厂 + 策略 + 观察者。

工厂模式负责创建不同的后端实例和算子实例,策略模式负责在运行时选择执行策略(同步 / 异步 / 流水线)和内存分配策略,观察者模式负责将推理事件通知给监控和日志系统。三者的协作流程是:客户端通过工厂创建推理引擎(引擎内部根据配置选择具体的策略实现),客户端注册观察者到引擎,引擎执行推理时通过策略接口调用具体的执行逻辑,执行过程中通过观察者接口发布事件。这种组合使得后端扩展(新增后端只需新增工厂和适配器)、策略扩展(新增执行策略只需新增策略类)、事件扩展(新增监控维度只需新增观察者)都互不干扰。

另一个常见的组合是装饰器 + 代理 + 责任链:代理控制对推理服务的访问(权限 / 限流),装饰器为推理请求添加日志 / 计时 / 缓存功能,责任链将请求的预处理→推理→后处理串联起来。三者层层嵌套,形成一个功能完整的请求处理管道,且每个环节都可以独立替换。

8.10 反模式与过度设计

设计模式是工具,不是教条。过度设计是指为简单的需求套上复杂的模式结构,导致代码难以理解和维护。判断是否过度设计的标准是:这个模式是否解决了当前真实存在的问题,还是仅仅为了 "看起来专业"?

比如,如果一个推理引擎只支持 CPU 后端,且未来半年内没有支持 GPU 的计划,那么引入抽象工厂 + 适配器的完整后端抽象就是过度设计 ------ 直接写 CPU 实现即可,等需要支持 GPU 时再重构(YAGNI 原则:You Aren't Gonna Need It)。

模式的性能开销也需要关注:虚函数的间接调用有额外的指令开销(虽然现代 CPU 的分支预测能很好地掩盖,但在极热路径上仍有影响),动态分配(如std::function的堆分配、装饰器的指针间接)有内存和时间开销。在推理引擎的热路径(如算子内部的循环)上,应该避免虚函数调用,改用 CRTP 静态多态或模板编程实现零开销抽象。

8.11 CRTP 静态多态

CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)是一种 C++ 特有的技术,实现编译期的静态多态。它的形式是:基类是一个模板类,模板参数是派生类本身 ------class Base { ... }; class Derived : public Base<Derived> { ... };。基类通过static_cast<Derived*>(this)将 this 指针转换为派生类指针,调用派生类的方法,从而在编译期确定调用目标,不需要虚函数表和运行时分发。

CRTP 的优势是零运行时开销:没有虚函数表指针(节省内存)、没有虚函数调用的间接跳转(节省指令)、编译器可以内联派生类的方法(进一步优化)。这在推理引擎的算子层非常有价值 ------ 算子的执行循环是最热的路径,每一纳秒都重要,CRTP 可以在保持多态灵活性的同时实现零开销。

CRTP 的劣势是失去了运行时多态的灵活性:所有类型必须在编译期确定,不能在运行时动态切换实现。因此,CRTP 适用于类型在编译期已知的场景(如算子的模板实例化),而运行时需要动态切换的场景(如根据配置选择后端)仍需使用虚函数动态多态。一个常见的做法是:在 SDK 的对外接口层用虚函数动态多态(支持运行时配置),在内部的热路径层用 CRTP 静态多态(追求极致性能),两者通过适配层衔接。

8.12 易错点分析

易错点一:单例的线程安全。在 C++11 之前,单例的双检锁实现容易因指令重排导致线程安全问题。C++11 之后,局部静态变量的初始化被保证为线程安全的,因此 Meyers Singleton(static T& instance() { static T inst; return inst; })是最简单且安全的实现。但需要注意单例的析构顺序问题 ------ 如果单例 A 的析构函数依赖单例 B,而 B 先于 A 析构,会导致访问已析构对象。解决方案是控制析构顺序或使用指针形式的单例(手动管理生命周期)。

易错点二:观察者的生命周期。观察者模式中,被观察者持有观察者的指针或引用。如果观察者在被观察者之前被销毁,被观察者在发布事件时会访问已销毁的观察者指针,导致 use-after-free。解决方案是:观察者在析构时主动从被观察者中注销自己(在观察者的析构函数中调用unsubscribe),或者使用std::weak_ptr管理观察者引用,发布事件前检查 weak_ptr 是否有效。

易错点三:责任链的性能。责任链中每个处理器都是一个虚函数调用,且处理器之间通过指针传递请求。如果责任链很长(比如有十几个处理器)且请求量很大(每秒数万次推理),虚函数调用的开销和缓存不命中(每个处理器对象在内存中不连续)会累积。优化方案是:将无状态的处理器合并为一个函数对象、使用模板元编程在编译期构建责任链(零开销)、或者在热路径上直接内联处理逻辑而不经过责任链。

易错点四:工厂中对象的所有权转移。工厂创建对象后返回std::unique_ptr是最安全的做法 ------ 明确表示所有权转移给调用方,调用方负责生命周期管理。但有些工厂返回裸指针或std::shared_ptr,容易导致所有权不清晰。如果返回裸指针,调用方可能忘记 delete 导致内存泄漏,或者被多方 delete 导致 double-free。如果返回 shared_ptr,可能导致循环引用(对象 A 持有 B 的 shared_ptr,B 持有 A 的 shared_ptr)。最佳实践是工厂返回 unique_ptr,需要共享时由调用方显式转换为 shared_ptr。

易错点五:装饰器与代理的混淆。装饰器和代理的 UML 类图结构完全相同(都是持有一个同接口的内部对象),但意图完全不同。装饰器的目的是为被装饰对象增加行为(如添加日志、计时),通常由客户端决定装饰哪些行为、按什么顺序装饰。代理的目的是控制对被代理对象的访问(如延迟加载、权限检查、远程访问),代理通常对客户端透明 ------ 客户端不知道自己在和代理打交道,而不是真实对象。混淆两者会导致设计意图不清晰,后续维护者难以理解代码的目的。

8.13 面试追问与回答

问:装饰器和代理的区别?

答:结构上两者完全相同 ------ 都实现与被包装对象相同的接口,都持有一个被包装对象的指针,都在方法中转发调用。但意图和使用场景有本质区别。装饰器的意图是增加行为:它在转发调用的前后添加额外逻辑(日志、计时、缓存、重试),且通常由客户端显式地、按需要的顺序层层包裹核心对象,客户端知道自己在使用装饰器。代理的意图是控制访问:它代表真实对象处理请求,可能在转发前做权限检查、延迟加载真实对象、或将请求转发到远程对象,且通常对客户端透明 ------ 客户端以为自己在和真实对象交互,实际上是代理。简单说:装饰器是 "我要给这个对象加点功能",代理是 "我要代替这个对象来控制谁能访问它、什么时候访问它"。

问:策略和模板方法的区别?

答:两者都用于封装算法的变化,但实现方式和适用场景不同。策略模式使用组合:上下文类持有一个策略接口的指针,算法的每个变体是一个独立的策略类,在运行时通过set_strategy切换。策略模式的优势是灵活 ------ 可以在运行时动态切换算法,策略之间完全独立,新增策略不需要修改上下文类。模板方法模式使用继承:基类定义算法的骨架(固定的步骤顺序),将可变的步骤声明为虚函数由子类实现。模板方法的优势是能控制算法的整体结构 ------ 基类保证所有子类遵循相同的步骤顺序,且可以在基类中实现公共步骤(钩子方法),子类只覆盖需要变化的步骤。选择标准:如果需要在运行时动态切换算法、或算法的各个变体之间没有共享的骨架结构,用策略;如果所有算法变体遵循相同的步骤顺序、只是某些步骤的实现不同,用模板方法。实际中两者可以结合:模板方法定义骨架,骨架中的某些步骤委托给策略对象。

问:观察者和回调的区别?

答:回调是一种语言机制(函数指针、functor、lambda),观察者是一种设计模式。观察者模式内部通常使用回调机制来实现通知 ------ 被观察者维护一个回调函数列表,事件发生时遍历调用。但观察者模式比单纯的回调多了一层结构:它定义了被观察者(Subject)和观察者(Observer)的角色,观察者可以注册和注销,被观察者在状态变化时主动通知所有观察者。回调可以是一对一的(一个事件对应一个回调函数),而观察者天然是一对多的(一个事件可以有任意多个观察者)。此外,观察者模式中的观察者通常是实现了特定接口的对象(有状态、有生命周期管理),而回调通常是无状态的函数或 lambda。在 C++ 中,std::function+std::vector<std::function<...>>是实现观察者模式的常见轻量方式,不需要显式定义观察者接口。

问:什么是 CRTP?

答:CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)是一种 C++ 模板技术,基类是一个模板类,模板参数是派生类自身。形式为template<typename Derived> class Base { ... }; class Derived : public Base<Derived> { ... };。基类中通过static_cast<Derived*>(this)将 this 转换为派生类指针,调用派生类的方法。这实现了编译期的静态多态 ------ 方法调用在编译期就绑定到派生类的实现,不需要虚函数表和运行时分发,因此是零开销抽象。CRTP 广泛用于:为派生类自动添加功能(如运算符重载、对象计数、单例实现)、在编译期实现策略模式(如Allocator策略)、实现 mixin(混入)模式。但 CRTP 失去了运行时多态的灵活性 ------ 所有类型必须在编译期确定,不能在运行时动态切换。

问:为什么说 RAII 是 C++ 最重要的设计模式?

答:RAII 解决了 C++ 中最根本的问题 ------ 资源管理的安全性和异常安全性。C++ 没有垃圾回收,内存和其他资源(文件句柄、锁、设备内存、网络连接)需要手动管理。手动管理的问题是:容易忘记释放导致泄漏,异常抛出时释放代码被跳过导致泄漏,多个返回路径时需要在每个路径都写释放代码导致重复和遗漏。RAII 将资源生命周期与对象生命周期绑定,构造函数获取资源,析构函数释放资源,而 C++ 保证对象离开作用域时析构函数一定被调用(无论正常返回还是异常)。这使得资源管理变成自动的、异常安全的、无遗漏的。C++ 标准库中的智能指针(unique_ptr/shared_ptr)、lock_guard、ifstream 等都是 RAII 的实现。可以说,不掌握 RAII 就不可能写出安全的 C++ 代码,因此它是 C++ 最重要的设计模式 ------ 甚至不是 "之一"。

问:如何在无虚函数的情况下实现多态?

答:有几种方式。第一,CRTP 静态多态:通过模板和static_cast在编译期绑定方法调用,零运行时开销,但类型必须在编译期确定。第二,std::variant+std::visit:将所有可能的类型存入 variant,用 visit 根据当前存储的类型调用对应的处理函数,这是一种 "可辨识联合" 式的多态,类型集合在编译期固定但运行时可以切换。第三,函数指针表(vtable 的手动实现):自己定义一个函数指针结构体,每个类型实例化一个函数指针表,对象持有指向函数指针表的指针,这本质上是虚函数的手动实现,有运行时开销但可以控制内存布局。第四,类型擦除:通过std::function或自定义的类型擦除容器,将不同类型的对象统一为一个接口,内部通过虚函数或函数指针实现分发。第五,宏和代码生成:在编译期为每种类型生成独立的代码路径,通过模板特化或 if constexpr 在编译期选择。选择哪种方式取决于是否需要运行时动态性、性能要求、类型集合是否固定。

问:设计模式在嵌入式 / 实时系统中有什么特殊考虑?

答:嵌入式和实时系统对设计模式的使用有更严格的约束。第一,内存约束:嵌入式系统内存有限,动态分配(new/malloc)可能被禁止或严格限制。很多设计模式依赖动态分配(如工厂创建对象、装饰器嵌套创建包装对象、观察者动态注册),在嵌入式中需要改用静态分配(预分配对象池、placement new)或栈上分配。第二,实时性约束:硬实时系统要求操作的执行时间有确定的上界。虚函数调用的间接跳转、动态分配的不确定时间、锁的不确定等待时间都可能破坏实时性。因此在热路径上应使用 CRTP 静态多态替代虚函数,使用预分配对象池替代动态分配,使用无锁数据结构替代锁。第三,异常安全:很多嵌入式编译器禁用异常(-fno-exceptions),此时 RAII 的异常安全优势减弱,但 RAII 的资源自动释放仍然有价值。第四,代码体积:嵌入式系统的 Flash 空间有限,模板元编程(如 CRTP、模板策略)会为每种类型生成独立代码,可能导致代码体积膨胀。需要权衡模板实例化的数量,必要时用类型擦除或虚函数减少代码重复。第五,可靠性:嵌入式系统通常要求高可靠性,设计模式的使用应尽量简单透明,避免过度抽象导致的代码难以验证和测试。

面试追问速查表
追问问题 核心考点 一句话回答要点
装饰器和代理的区别? 结构相同、意图不同 装饰器增加行为,代理控制访问
策略和模板方法的区别? 组合 vs 继承 策略运行时切换算法,模板方法固定骨架
观察者和回调的区别? 模式 vs 语言机制 观察者是一对多、有注册注销,回调是一对一
什么是 CRTP? 静态多态、零开销 模板参数为派生类自身,编译期绑定
为什么 RAII 最重要? 资源安全、异常安全 资源生命周期绑定对象生命周期
无虚函数如何实现多态? 多种替代方案 CRTP、variant+visit、函数指针表、类型擦除
嵌入式/实时系统特殊考虑? 内存、实时性、异常 静态分配、CRTP、无锁、控制代码体积
面试追问知识图谱
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    8.13 面试追问与回答                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  模式对比类                                                       │
│  ├─ 装饰器 vs 代理    → 增加行为 vs 控制访问                      │
│  ├─ 策略 vs 模板方法  → 组合(运行时切换) vs 继承(固定骨架)        │
│  └─ 观察者 vs 回调    → 一对多模式 vs 一对一机制                  │
│                                                                   │
│  C++ 特性类                                                       │
│  ├─ CRTP             → 模板静态多态,零运行时开销                 │
│  ├─ RAII             → 资源生命周期绑定对象生命周期               │
│  └─ 无虚函数多态      → CRTP / variant+visit / 函数指针表 / 类型擦除│
│                                                                   │
│  场景约束类                                                       │
│  └─ 嵌入式/实时系统   → 静态分配 / CRTP / 无锁 / 控制代码体积     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
问:工厂模式和策略模式的区别?

答:两者都属于创建型 / 行为型的典型代表,但解决的问题完全不同。工厂模式是创建型模式,核心是封装对象的创建过程,让客户端通过工厂接口获取对象实例,而不直接使用 new 和具体类名,从而解耦对象的创建与使用。策略模式是行为型模式,核心是封装算法的实现,让客户端在运行时切换不同的算法变体,从而解耦算法的定义与调用。简单说:工厂模式回答 "对象怎么创建",策略模式回答 "算法怎么执行"。在 AI 推理 SDK 中,两者经常配合使用 ------ 工厂负责创建具体的后端实例(如 TensorRT 后端、ONNX Runtime 后端),策略负责在运行时选择执行方式(同步、异步、流水线)。

cpp 复制代码
// 工厂模式:创建对象
class IBackend { public: virtual void infer() = 0; };
class TensorRTBackend : public IBackend { public: void infer() override {} };
class BackendFactory {
public:
    static std::unique_ptr<IBackend> create(const std::string& type) {
        if (type == "tensorrt") return std::make_unique<TensorRTBackend>();
        return nullptr;
    }
};
// 策略模式:切换算法
class IExecStrategy { public: virtual void execute() = 0; };
class SyncStrategy : public IExecStrategy { public: void execute() override {} };
class AsyncStrategy : public IExecStrategy { public: void execute() override {} };
class InferEngine {
public:
    void set_strategy(std::unique_ptr<IExecStrategy> s) { strategy_ = std::move(s); }
private:
    std::unique_ptr<IExecStrategy> strategy_;
};
问:单例的线程安全实现有哪几种?

答:主要有三种。第一,Meyers Singleton:C++11 之后局部静态变量的初始化是线程安全的,编译器会生成 guard 变量保证只初始化一次,最简单且推荐。第二,std::call_once + std::once_flag:显式控制一次性初始化逻辑,适合初始化逻辑较复杂的场景。第三,双检锁(DCLP):先检查指针是否为空,为空则加锁再检查一次,需要配合 std::atomic 和 memory_order_acquire/release 防止指令重排,容易写错,不推荐手写。在 AI 推理 SDK 中,设备管理器、日志器、算子注册表等全局唯一组件常用 Meyers Singleton 实现。

cpp 复制代码
// 方式一:Meyers Singleton(推荐)
class Logger {
public:
    static Logger& instance() {
        static Logger inst;  // C++11 起线程安全
        return inst;
    }
    void log(const std::string& msg) { std::cout << msg << "\n"; }
private:
    Logger() = default;
};
// 方式二:call_once
class Registry {
public:
    static Registry& instance() {
        static Registry* inst = nullptr;
        static std::once_flag flag;
        std::call_once(flag, [] { inst = new Registry(); });
        return *inst;
    }
private:
    Registry() = default;
};
问:装饰器和代理的区别?

答:结构上两者完全相同 ------ 都实现与被包装对象相同的接口,都持有一个被包装对象的指针,都在方法中转发调用。但意图和使用场景有本质区别。装饰器的意图是增加行为:它在转发调用的前后添加额外逻辑(日志、计时、缓存、重试),且通常由客户端显式地、按需要的顺序层层包裹核心对象,客户端知道自己在使用装饰器。代理的意图是控制访问:它代表真实对象处理请求,可能在转发前做权限检查、延迟加载真实对象、或将请求转发到远程对象,且通常对客户端透明 ------ 客户端以为自己在和真实对象交互,实际上是代理。简单说:装饰器是 "我要给这个对象加点功能",代理是 "我要代替这个对象来控制谁能访问它、什么时候访问它"。

cpp 复制代码
// 装饰器:增加行为
class LoggingDecorator : public IInferService {
public:
    std::vector<float> infer(const std::vector<float>& input) override {
        std::cout << "[Log] start\n";
        auto result = inner_->infer(input);
        std::cout << "[Log] end\n";
        return result;
    }
private:
    std::shared_ptr<IInferService> inner_;
};
// 代理:控制访问
class AuthProxy : public IInferService {
public:
    std::vector<float> infer(const std::vector<float>& input) override {
        if (!check_permission()) throw std::runtime_error("denied");
        return real_->infer(input);
    }
private:
    std::shared_ptr<IInferService> real_;
};
问:RAII 的原理是什么?

答:RAII(Resource Acquisition Is Initialization,资源获取即初始化)的核心思想是:将资源的生命周期与对象的生命周期绑定 ------ 在构造函数中获取资源,在析构函数中释放资源。C++ 保证对象离开作用域时析构函数一定被调用,无论正常返回还是抛出异常,因此资源释放是自动的、异常安全的、无遗漏的。在 AI 推理 SDK 中,RAII 广泛应用于设备内存管理(std::unique_ptr<float, CudaDeleter> 管理 cudaMalloc 分配的显存)、锁管理(std::lock_guard 自动加解锁)、文件句柄和网络连接管理。

cpp 复制代码
// 自定义 deleter 管理显存
struct CudaDeleter {
    void operator()(float* p) const { cudaFree(p); }
};
class CudaTensor {
public:
    explicit CudaTensor(size_t n) : data_(new float[n], CudaDeleter()) {}
private:
    std::unique_ptr<float[], CudaDeleter> data_;
};
// lock_guard 自动管理锁
void infer() {
    std::lock_guard<std::mutex> lock(mtx_);  // 离开作用域自动解锁
    // 执行推理...
}
问:Pimpl 的优缺点是什么?

答:Pimpl(Pointer to Implementation,指针实现)将类的实现细节隐藏在一个不透明的指针后面,头文件只声明 struct Impl; 前向声明和 std::unique_ptr<Impl> 成员。优点是:第一,编译防火墙 ------ 头文件不再包含实现所需的其他头文件,修改实现细节不会导致包含该头文件的其他文件重新编译,大幅缩短编译时间;第二,ABI 稳定 ------ 类的大小和布局不随实现细节变化(始终是一个指针的大小),可以在不重新编译客户端代码的情况下更新库实现,对二进制分发的 SDK 至关重要;第三,隐藏实现细节 ------ 私有成员变量和方法不再暴露在头文件中。缺点是:第一,每次访问成员需要一次指针间接跳转,有轻微性能开销;第二,需要额外的动态分配(Impl 对象在堆上),有内存和时间开销;第三,拷贝语义需要自定义(默认拷贝会浅拷贝指针导致 double-free),通常需要实现深拷贝或禁用拷贝。在 AI 推理 SDK 中,顶层 InferenceEngine 类使用 Pimpl 是标准实践。

cpp 复制代码
// 头文件 inference_engine.h
class InferenceEngine {
public:
    InferenceEngine();
    ~InferenceEngine();
    InferenceEngine(const InferenceEngine&) = delete;
    InferenceEngine& operator=(const InferenceEngine&) = delete;
    void infer(const std::vector<float>& input);
private:
    struct Impl;
    std::unique_ptr<Impl> pimpl_;
};
// 源文件 inference_engine.cpp
struct InferenceEngine::Impl {
    std::unique_ptr<IBackend> backend;
    std::mutex mtx;
    size_t batch_size = 1;
};
InferenceEngine::InferenceEngine() : pimpl_(std::make_unique<Impl>()) {}
InferenceEngine::~InferenceEngine() = default;
void InferenceEngine::infer(const std::vector<float>& input) {
    std::lock_guard<std::mutex> lock(pimpl_->mtx);
    pimpl_->backend->infer(input);
}

8.5 小结

本节系统梳理了创建型、结构型、行为型三类设计模式在 AI 推理 SDK 和 Agent 工具运行时中的选择原则。选择模式的核心不是套用经典模板,而是先识别当前要解决的真实工程问题,再判断该问题属于对象构造、结构组织还是行为编排,最后结合性能敏感度和扩展性要求确定具体模式。

创建型模式解决对象构造的灵活性与一致性:当推理引擎需要支持多后端(CPU、GPU、NPU)时,用抽象工厂 + 注册表统一创建张量、算子和内存分配器;当推理配置项过多时,用建造者链式构建并做参数校验;当需要全局唯一的设备管理器、日志器或算子注册表时,用单例;当需要基于默认配置快速复制复杂对象时,用原型。

结构型模式解决多后端 API 统一和功能叠加:适配器把 TensorRT、ONNX Runtime、OpenVINO 等异构 API 收敛到统一接口,是推理 SDK 中最基础的结构型模式;装饰器为推理请求叠加日志、计时、重试、缓存等横切关注点;代理控制远程推理、懒加载和权限访问;外观封装 SDK 内部子系统;桥接分离请求抽象与后端执行;组合统一计算图节点与子图;享元共享张量描述符等小对象。

行为型模式解决推理流程编排和工具调用链:策略在运行时切换执行、内存分配和精度校准算法;观察者把推理事件和工具事件通知给监控、日志和 UI;责任链串联鉴权、限流、预处理、推理、后处理等环节;模板方法固定后端推理流程骨架;状态管理推理请求生命周期;命令把工具调用封装为可排队、可重试、可审计的对象;访问者实现计算图优化 pass;中介者协调 Agent 工具间的协作。

在 AI 推理 SDK 和 Agent 工具运行时中,三类模式往往组合使用。下面给出一个模式选型决策流程图,帮助读者根据实际场景快速定位合适的模式:

flowchart TD A[遇到设计问题] --> B{问题属于哪一类?} B -->|对象创建| C{创建方式有何要求?} C -->|多后端/多产品族| C1[抽象工厂 + 注册表] C -->|参数多/配置复杂| C2[建造者 Builder] C -->|全局唯一组件| C3[单例 Singleton] C -->|复制已有复杂对象| C4[原型 Prototype] B -->|结构组织| D{结构诉求是什么?} D -->|统一异构后端API| D1[适配器 Adapter] D -->|动态叠加横切功能| D2[装饰器 Decorator] D -->|控制访问/懒加载/远程| D3[代理 Proxy] D -->|封装子系统复杂度| D4[外观 Facade] D -->|抽象与实现独立变化| D5[桥接 Bridge] D -->|统一叶子与组合节点| D6[组合 Composite] D -->|共享大量小对象| D7[享元 Flyweight] B -->|行为编排| E{行为诉求是什么?} E -->|运行时切换算法| E1[策略 Strategy] E -->|一对多事件通知| E2[观察者 Observer] E -->|请求多环节处理| E3[责任链 Chain] E -->|固定流程骨架| E4[模板方法 Template] E -->|对象状态迁移| E5[状态 State] E -->|操作可排队/可撤销| E6[命令 Command] E -->|对元素新增操作| E7[访问者 Visitor] E -->|对象间解耦协作| E8[中介者 Mediator] C1 --> F[结合性能与扩展性约束] C2 --> F C3 --> F C4 --> F D1 --> F D2 --> F D3 --> F D4 --> F D5 --> F D6 --> F D7 --> F E1 --> F E2 --> F E3 --> F E4 --> F E5 --> F E6 --> F E7 --> F E8 --> F F --> G{热路径性能敏感?} G -->|是| H[优先 CRTP 静态多态 / 模板 / 零开销抽象] G -->|否| I[使用虚函数动态多态,保持可扩展性] H --> J[落地实现并持续演进] I --> J

选型时还需注意:热路径(如算子内部循环)优先用 CRTP 静态多态或模板实现零开销抽象,避免虚函数和动态分配;对外接口层保留虚函数动态多态以支持运行时配置;资源管理一律用 RAII 绑定生命周期;避免为简单需求套用复杂模式,遵循 YAGNI 原则,等真实需求出现时再引入对应模式。

8.14 本节总结

本节系统讲解了设计模式在 AI 推理 SDK 和 Agent 工具运行时中的落地实践。创建型模式(工厂、建造者、单例、原型)解决对象构造的灵活性和一致性问题,其中抽象工厂 + 注册表是多后端推理引擎的标准架构。结构型模式(适配器、装饰器、代理、外观、桥接、组合、享元)解决多后端 API 统一、功能叠加、复杂度封装等问题,适配器和装饰器是推理 SDK 中最高频使用的结构型模式。行为型模式(策略、观察者、责任链、模板方法、状态、命令、访问者、中介者)解决推理流程编排、事件通知、工具调用链等问题,责任链和观察者在 Agent 运行时中尤为重要。RAII 是 C++ 特有的资源管理基石,Pimpl 提供编译防火墙和 ABI 稳定,类型擦除统一不同签名的工具函数,CRTP 在热路径上实现零开销静态多态。设计模式的使用应遵循 "解决真实问题" 的原则,避免过度设计,在性能敏感的热路径上优先考虑零开销方案。掌握这些模式的组合使用,是构建可扩展、可维护、高性能 AI 系统的关键能力。

相关推荐
猫头虎1 小时前
FDE 是什么岗位?Forward Deployed Engineer 岗位标准、能力模型、交付物规范与冲突处置规则全解析
人工智能·开源·开源软件·ai编程·ai写作·gpu算力·agi
G31135422731 小时前
当声音、图片和视频都能被生成,信任将从“看起来真实”转向“能够验证”
人工智能
揽秀亭长1 小时前
视频转脚本实际怎么做?对比3个不同方案,拆解视频转脚本不同技术流程
人工智能·音视频
知几蜗牛1 小时前
GPU抢不到就换一种:训练任务需要先声明可替代性
人工智能
知几蜗牛1 小时前
Agent不是多想几步就能上线:用状态机管住自动行动
人工智能
adinnet20261 小时前
客服与工单:响应时长与满意度问数
数据库·人工智能
知几蜗牛1 小时前
数据不能集中,算力也不统一:联邦学习终于面对运维现实
人工智能
知几蜗牛1 小时前
OpenAI开始公开模型失配个案:真正重要的是这套报告制度
人工智能
知几蜗牛1 小时前
广告开始和你对话:Sponsored Agents改变的不是文案
人工智能