跳转到主要内容

茶 语 学 籍

智能体架构原理剖析

五层目录分层 · 依赖注入容器 · Agent 循环编排

以茶载道,以构启思。
从核心基础设施到 AI 推理层,从接口抽象到 Agent 循环调度,系统剖析桌面智能体的结构原理。

CHAPTER 01

功能概述

分层解耦的桌面智能体架构,让基础设施、业务逻辑与 AI 推理各司其职

架构定位

Tazhiyou 是一款基于 C++ 与 Qt6 构建的桌面智能助手。其架构遵循"基础设施 → 业务模型 → AI 集成 → 视图交互 → 控制编排"的五目录分层设计,通过 ServiceLocator 依赖注入容器 管理组件生命周期,以 IService / IRepository / IModule 三套接口契约约束跨层协作,最终由 AppController 统一编排 7 个功能模块。AI 能力由 Agent 循环驱动,首轮检索知识库(RAG),后续轮次调用工具完成多步推理。

5
目录分层
7
功能模块
3
接口契约
5
Agent 最大轮次

设计目标

  • ·关注点分离:基础设施与业务逻辑解耦,UI 与 AI 推理解耦,每层可独立演进
  • ·接口契约:三套抽象接口(服务/仓储/模块)约束跨层协作边界,编译期类型安全
  • ·依赖反转:高层模块不直接依赖低层实现,统一通过容器注入,支持测试替换
  • ·本地优先:默认 Ollama 本地推理,离线可用;在线模型作为可选增强

核心能力

  • 推理Agent 循环多轮工具调用,最大 5 轮,首轮 RAG 注入知识上下文
  • 知识KnowledgePipeline 7 阶段流水线,维基百科获取到 Embedding 检索
  • 工具ToolRegistry 运行时动态注册,技能清单注入系统提示
  • 编排AppController 统一注册/初始化/关闭 7 个功能模块

CHAPTER 02

系统架构

五目录分层 · 三接口契约 · 依赖注入容器

源码五目录分层结构

app/ · 控制编排层
AppController + ModuleManager · 7 模块注册与生命周期编排
views/ · 视图层
IModule 实现 · 按功能域分子目录
交互
ai/ · AI 集成层
ModelManager · Agent 循环 · ToolRegistry
推理
models/ · 业务模型层
IRepository 实现 · 知识/文件/系统等领域
业务
core/ · 基础设施层
arch (ServiceLocator/ErrorHandler/Logger) · interfaces · security · utils · config

五目录职责划分

core/基础设施:ServiceLocator 依赖注入、ErrorHandler 错误处理、StructuredLogger 日志、SecurityValidator 安全校验、接口定义(IService/IRepository/IModule)
models/业务模型:knowledge(知识库)/filemgmt(文件管理)/converter(格式转换)/pcctrl(电脑控制)/sysbox(系统工具箱)/sysmon(系统监控)等领域仓储
ai/AI 集成:ModelManager(LLM 管理)、ToolRegistry(工具注册)、IntentParser(意图解析)、CommandExecutor(命令执行)、SkillManager(技能管理)
views/视图交互:按功能域分子目录,每个模块实现 IModule 接口,由 AppController 统一注册
app/控制编排:AppController 作为应用入口,持有 ModuleManager,负责模块注册、初始化顺序、优雅关闭

三套接口契约

IService — 服务生命周期契约:serviceName() / initialize() / shutdown() / isReady()
IRepository<TEntity, TKey> — 仓储数据访问契约:save / findById / find / remove / count / exists + QueryOptions 分页
IModule — 模块视图契约:meta / initialize / createView / shutdown

概念上形成三层契约关系:IRepository 约束数据访问层,IService 约束服务编排层,IModule 约束视图交互层。三者通过 ServiceLocator 容器解耦组装。

ServiceLocator 依赖注入容器

ServiceLocator 是核心的单例依赖注入容器,支持三种生命周期注册模式,通过 ServiceExtractor 类型擦除(shared_ptr<void> + if constexpr 编译期检测)实现跨层解耦,内部使用 QReadWriteLock 保证线程安全。

Singleton
registerSingleton<I>(factory)
全局唯一实例,首次 resolve 时创建
Transient
registerTransient<I>(factory)
每次 resolve 创建新实例
Instance
registerInstance<I>(instance)
注入预创建的外部实例
resolve<I>() → shared_ptr<I> // 解析依赖,未注册则抛异常
tryResolve<I>() → shared_ptr<I> // 宽松解析,未注册返回 nullptr
initializeAll() // 按注册顺序调用 IService::initialize
shutdownAll() // 逆序调用 IService::shutdown

CHAPTER 03

核心实现逻辑

Agent 循环 · RAG 检索增强 · 模块编排 · 模型管理

Agent 循环工作流程

用户输入 → onSend()
触发 startAgentLoop()
首轮:RAG 检索
KnowledgeBase::search(query, 3) 召回 Top3 知识
知识注入
技能清单注入
已启用技能 ID/名称/触发词/描述写入系统提示
能力感知
chatWithTools() 调用 LLM
stream=false 非流式 · 超时 60s · maxTokens=4096
无工具调用
直接返回最终回复
有工具调用
executeToolCalls() · 危险操作二次确认
continueAgentLoop(iteration+1)
最多 5 轮(kMaxAgentIterations)· 工具结果作为 role=Tool 消息回传

Agent 循环关键参数

迭代kMaxAgentIterations = 5,防止无限循环
流式stream = false,非流式响应,等待完整 JSON 后解析
超时chat 30s / chatWithTools 60s,避免长时阻塞
上下文maxRounds = max(10, AppConfig::contextRounds())
工具分级destructive 操作二次确认 / longRunning 后台执行

ModelManager 模型注册

ModelManager 管理 3 个 LLM 端点,默认使用本地 Ollama 推理,确保离线可用。在线模型(OpenAI/DeepSeek)作为可选增强。

ollama_local → Ollama · localhost:11434 · qwen2.5:7b (默认)
openai_online → OpenAI · api.openai.com/v1 · gpt-4o-mini
deepseek → DeepSeek · api.deepseek.com/v1 · deepseek-chat
────────────────────────────
默认: m_currentId = "ollama_local"
maxTokens = 4096

AppController 模块编排

AppController 作为应用入口,持有 ModuleManager,在初始化阶段按顺序注册 7 个功能模块,每个模块实现 IModule 接口。注册后统一调用 initialize(),关闭时逆序调用 shutdown()。

FileManagement
文件管理
SystemMonitor
系统监控
PcControl
电脑控制
Knowledge
知识增强
AIAssistant
AI 助手
Settings
设置
ArchMonitor
架构监控
预留扩展

CHAPTER 04

关键代码解析

从接口定义到循环调度的核心实现

片段 1 ServiceExtractor 类型擦除

ServiceLocator.h · makeExtractor
C++
// 类型擦除: shared_ptr<void> 存储 + if constexpr 编译期检测 IService
using ServiceExtractor = std::function<std::shared_ptr<IService>(const std::shared_ptr<void>&)>;

template<typename Interface>
ServiceExtractor makeExtractor() {
    return [](const std::shared_ptr<void>& p) -> std::shared_ptr<IService> {
        // 编译期判断 Interface* 是否可隐式转换为 IService*
        if constexpr (std::is_convertible_v<Interface*, IService*>) {
            return std::static_pointer_cast<Interface>(p);
        }
        return nullptr;
    };
}

// 容器内部以 shared_ptr<void> 统一存储, 提取时类型擦除还原
QHash<QString, std::shared_ptr<void>> m_instances;
QReadWriteLock m_lock;  // 读写锁保证线程安全

技术要点:使用 shared_ptr<void> 类型擦除存储异质实例,避免 dynamic_pointer_cast<IService> 的非法转换。通过 if constexpr 在编译期判断接口是否继承 IService,类型安全且零运行时开销。

片段 2 Agent 循环调度

AIAssistantModule.cpp · startAgentLoop / continueAgentLoop
C++
// Agent 循环最大轮次
static constexpr int kMaxAgentIterations = 5;

void startAgentLoop(const QString& userQuery) {
    // 首轮 RAG: 召回知识库 Top3 相关条目
    auto results = KnowledgeBase::instance().search(userQuery, 3);

    // 构建系统提示: 知识上下文 + 已启用技能清单
    QString systemPrompt = buildSystemPrompt(results);

    // 保留上下文轮数
    const int maxRounds = qMax(10, AppConfig::contextRounds());

    continueAgentLoop(0);  // 进入第一轮
}

void continueAgentLoop(int iteration) {
    if (iteration >= kMaxAgentIterations) {
        emit reply("已达到最大推理轮次限制");
        return;
    }
    // 非流式调用 (stream=false), 超时 60s
    m_model->chatWithTools(m_messages, /*timeout=*/60000);
    // QNetworkReply::finished 信号回调 onToolReply
}

void onToolReply(const QJsonArray& toolCalls) {
    if (toolCalls.isEmpty()) {
        emit reply(finalText);  // 无工具调用, 结束
    } else {
        executeToolCalls(toolCalls, iteration);  // 危险操作二次确认
        continueAgentLoop(iteration + 1);  // 下一轮
    }
}

设计要点:首轮注入 RAG 检索结果与技能清单,使 LLM 感知可用知识与能力;非流式(stream=false)简化工具调用解析;工具结果以 role=Tool 消息回传,LLM 在下一轮自主分析结果决定是否继续调用或给出最终回复。5 轮上限防止死循环。

片段 3 IService 生命周期接口

core/interfaces/IService.h
C++
class IService {
public:
    virtual ~IService() = default;

    // 服务名称 (用于日志与诊断)
    virtual QString serviceName() const = 0;

    // 初始化: 加载资源/建立连接/注册子组件
    virtual Result<void> initialize() = 0;

    // 关闭: 释放资源/断开连接/持久化状态
    virtual Result<void> shutdown() = 0;

    // 就绪状态查询
    virtual bool isReady() const = 0;
};

// ServiceLocator.initializeAll() 按注册顺序调用 initialize()
// ServiceLocator.shutdownAll()   逆序调用 shutdown() (后注册先关闭)

契约意义:所有注册到容器的服务必须实现 IService,确保生命周期可控。逆序关闭保证依赖关系——被依赖的服务后关闭,避免悬空引用。

片段 4 IRepository 泛型仓储

core/interfaces/IRepository.h
C++
template<typename TEntity, typename TKey>
class IRepository {
public:
    virtual Result<TKey> save(const TEntity& entity) = 0;
    virtual Result<TEntity> findById(const TKey& id) const = 0;
    virtual Result<QVector<TEntity>> find(const QueryOptions& opts = {}) = 0;
    virtual Result<void> remove(const TKey& id) = 0;
    virtual Result<int> count() = 0;
    virtual Result<bool> exists(const TKey& id) { /* 默认: findById */ }
};

// QueryOptions: 分页 (limit/offset) + 过滤 + 排序
struct QueryOptions {
    int limit = -1;      // -1 不限制
    int offset = 0;
    QString orderBy;
    QString filter;
};

领域实践:KnowledgeStore 实现 IRepository<KnowledgeEntry, qint64>,通过 QObject + IRepository 多继承(QObject 必须为第一基类)。findById() 为纯读操作,get() 才更新访问统计(hit_count)——两者职责分离。

CHAPTER 05

注意事项

架构设计中的关键约束与潜在陷阱

QObject 多继承顺序

实现 IRepository 时,QObject 必须为第一基类(Qt 元对象系统要求)。若顺序颠倒,moc 生成的元对象代码将无法正确工作,信号槽失效。KnowledgeStore 的正确写法:class KnowledgeStore : public QObject, public IRepository<KnowledgeEntry, qint64>

dynamic_pointer_cast 禁忌

shared_ptr<void> 执行 dynamic_pointer_cast<IService>非法的——void* 不携带运行时类型信息(RTTI)。必须使用 ServiceExtractor 类型擦除方案,通过 if constexpr (std::is_convertible_v<Interface*, IService*>) 在编译期判断,再 static_pointer_cast 还原。

Agent 轮次与超时

Agent 循环硬上限为 5 轮(kMaxAgentIterations),防止 LLM 反复调用工具陷入死循环。chatWithTools 超时 60s,若 LLM 响应缓慢会触发超时中断。非流式(stream=false)意味着用户需等待完整响应,长文本生成时感知延迟较高——这是为简化工具调用解析做的取舍。

模块注册完整性

AppController 注册的 7 个模块需保持与视图层一致。FileConverterModule 模型层(converter/)完整存在但未注册到 AppController——这是已知的遗留缺口,意味着文件转换功能在视图层不可达。扩展新模块时务必同步更新 AppController 的注册列表。

关闭顺序依赖

shutdownAll() 逆序关闭——后注册的服务先关闭。这要求注册顺序遵循依赖方向:被依赖的基础服务(如 StructuredLogger、KnowledgeStore)先注册,依赖它们的业务服务后注册。违反此顺序会导致关闭时悬空引用。

本地模型默认策略

默认 m_currentId = "ollama_local",需用户本地运行 Ollama 服务(端口 11434)。若未启动,AI 功能不可用但应用其余模块不受影响。在线模型(OpenAI/DeepSeek)需配置 API Key,且存在网络延迟与隐私考量——架构上作为可选增强而非必需依赖。

CHAPTER 06

常见问题解答

架构原理层面的常见疑问与解答

Q1:为什么是五目录分层而不是传统的 MVC/MVVM?

桌面智能助手涉及 AI 推理、知识检索、系统控制等异质能力,传统 MVC 难以清晰划分。五目录(core/models/ai/views/app)按职责而非模式划分:core 提供基础设施,models 承载业务仓储,ai 独立 AI 集成,views 处理交互,app 统一编排。这种分层使各能力域可独立演进,且与依赖注入容器天然契合。

Q2:ServiceLocator 为什么用类型擦除而非模板全特化?

模板全特化要求所有服务类型在编译期已知,且容器需为每种类型生成独立存储,二进制膨胀。类型擦除(shared_ptr<void>)统一存储异质实例,运行时灵活注册;ServiceExtractor 配合 if constexpr 在编译期做类型安全检查,兼顾灵活性与零开销。代价是提取时需知道目标接口类型。

Q3:Agent 循环为什么限制为 5 轮而非更多?

5 轮是经验阈值:绝大多数工具调用任务(查天气、读文件、执行命令)2-3 轮即可完成;复杂的多步推理(如"搜索知识→生成文档→导出")通常不超过 5 轮。过多的轮次往往意味着 LLM 陷入无效循环或任务定义不清。5 轮上限在功能完整性与资源消耗间取得平衡,同时 chatWithTools 的 60s 超时进一步约束单轮耗时。

Q4:为什么 LLM 调用使用非流式而非流式?

Agent 循环需要解析完整的工具调用 JSON(tool_calls 数组),流式增量解析 JSON 复杂且易错——部分 JSON 片段无法合法解析。非流式(stream=false)等待完整响应后一次性解析,逻辑简洁可靠。代价是用户需等待完整响应,长文本生成时感知延迟较高。这是工程实用性的取舍。

Q5:IService / IRepository / IModule 三套接口如何协作?

三者职责正交:IService 约束服务生命周期(初始化/关闭),IRepository 约束数据访问(增删改查+分页),IModule 约束视图交互(元信息/创建视图)。一个组件可同时实现多个接口——例如 KnowledgeStore 实现 IRepository(数据访问)与 IService(生命周期);AIAssistantModule 实现 IModule(视图)与 IService(生命周期)。ServiceLocator 按 IService 维度统一管理初始化与关闭。

Q6:ErrorHandler 与 StructuredLogger 在架构中的角色?

ErrorHandler 提供 guard<T>() 模板(try/catch + 自动上报)与 ErrorScope RAII(自动提取 Win32 系统错误),覆盖 9 个错误类别,500 条环形缓冲。StructuredLogger 提供 12 个 LogCategory 与 PerfTracer(纳秒级性能追踪)。两者均为 core 层基础设施,通过 ServiceLocator 单例注入,被所有上层模块共享——确保错误与性能数据全局可观测。