# quick_agent **Repository Path**: mengdj/quick_agent ## Basic Information - **Project Name**: quick_agent - **Description**: No description available - **Primary Language**: C++ - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-24 - **Last Updated**: 2026-10-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # quickagent 轻量级 **C++14 图结构 Agent 框架**,参考 [neograph](https://pypi.org/project/neograph-engine/) 的 LangGraph 式执行模型(节点 / 通道 / 归约器 / 条件边 / 超级步 / 检查点 / 重试), 内置 **ReAct 工具循环** 与 **RAG 检索增强流水线**。无强制第三方依赖 (nlohmann/json 已 vendored;HTTP 传输为可选组件,通过 `QAGENT_HTTP_BACKEND` 启用 WinHTTP / libcurl 或注入自定义实现)。 ## 特性 - **图引擎 (`qa::core`)** - 节点读写命名通道(channel),超级步(superstep)批量执行后经归约器提交写回; 同批次节点读取一致的 pre-superstep 快照(Pregel 隔离) - `EngineConfig::concurrent_supersteps` 开启同层并发执行;`MapNode` 对数组 通道做有并发上限的 fan-out - 归约器:`overwrite` / `append` / `merge`,可自定义 - 无条件边、条件路由边、fan-out、循环(`max_steps` 兜底) - 每个超级步自动检查点,`resume()` 从任意快照继续;检查点存储为 `CheckpointStore` 接口,内置 `JsonFileCheckpointStore` 跨进程落盘 (JSONL 追加 + 侧车文件锁 + `prune()` 保留策略) - 节点级重试策略(次数 + 线性退避),失败统一收敛为 `stop_reason="error"` + `RunResult.errors`,不丢检查点 - 运行级协作式取消 / 截止时间:`RunOptions::with_timeout(ms)` 或注入 `RunControl`,节点通过 `ctx.stop_requested()` / `ctx.throw_if_stopped()` 响应;停止原因 `cancelled` / `timeout`,已提交检查点仍可续跑 - 人工介入(HITL):节点 `ctx.interrupt(payload)` 暂停运行,检查点持久化待 处理节点与 payload;`resume(cp, ResumeCommand)` 按节点名回填输入,同一 超级步的多个暂停节点可分别恢复 - 子图(subgraph):`add_subgraph(name, child, SubgraphConfig)` 把编译好的图 作为父图节点,显式声明输入 / 输出通道映射;子图的错误、halt、暂停与流式 chunk 都映射到父图 - 线程 / 会话隔离:`RunOptions::thread_id` 为每次运行打会话标记,一个引擎与 一个 `CheckpointStore` 可服务多个会话;检查点、`RunResult`、`StreamEvent` 与 `ctx.thread()` 均携带会话标识,可按会话恢复 - 图级流式输出:`run_stream(s, on_event)` / `resume_stream(...)` 按运行顺序 推送生命周期事件与节点 `ctx.emit(chunk)` 产生的增量内容;并发超级步下 回调仍串行化,宿主无需自行加锁 - 每次运行生成 `RunResult.run_id`,`on_run_begin(run_id)` 与 `on_retry(node, attempt, error)` 钩子可用于日志与指标关联 - `Observer` 生命周期钩子(节点开始/结束、超级步提交、运行结束), 多播注册;并发超级步下通知串行化,`JsonTraceObserver` 输出带耗时与 run id 的线程安全 JSON span,`MetricsObserver` 汇总步数 / 节点错误 / 重试 / 耗时 - **LLM 抽象 (`qa::llm`)** - `LLMProvider` 接口;`OpenAIProvider` 兼容任意 OpenAI 协议端点 (OpenAI / DeepSeek / Qwen / vLLM / Ollama `/v1` / 本地代理) - `AnthropicProvider` 原生支持 Anthropic Messages API(`x-api-key` + `anthropic-version`、顶层 `system`、content blocks、工具调用、SSE 事件) - `chat_stream` 流式接口(SSE 增量回调),默认降级为阻塞 `chat()`;OpenAI 自动请求 `stream_options.include_usage` 并解析末包 usage - 可重试错误(HTTP 429/5xx、传输失败)按 `RetryConfig` 退避重试; 已产生流式增量后不再重放 - `HttpClient` 抽象:`post` / `post_stream` / `get`,内置 WinHTTP 与 libcurl 后端(通过 `QAGENT_HTTP_BACKEND` opt-in);`HttpTimeouts` 可配置 连接 / 整体超时,`HttpRequestOptions::should_stop` 可中止在途请求, 也可注入 Qt / 自定义传输 - `MockProvider` 脚本化离线提供方,便于测试与演示 - JSON Schema 结构化输出:`ChatOptions::response_schema` 生成 `response_format: json_schema`;`chat_structured()` 本地校验并把违规项 回灌修复,非 OpenAI 兼容 provider 也可用;支持 `pattern`、 `if/then/else`、`dependentRequired` 与外部 `$ref` 文档; `ChatOptions::should_stop` 在每次调用前生效 - **ReAct Agent (`qa::agent`)** - `Tool` / `ToolRegistry` 注册 JSON Schema 工具,模型发工具调用 -> 执行 -> 观测回灌 -> 终答;未知工具与工具异常转为错误观测,不中断循环; `execute()` 默认先按工具 schema 校验参数,失败时返回 `invalid_arguments` 与违规路径,不调用工具 - `run()` / `run_stream()`;`continue_with()` / `continue_with_stream()` 承接上一轮 `history` 做多轮对话 - `HistoryLimits` 按消息数 / 字符数限制送入模型的上下文,保留 system 与 最新一轮,工具调用与结果不会拆散;完整历史仍在 `AgentResult` - `ToolExecutionPolicy` 可选择并发执行同一轮的多个工具,并限制并发数; 可配置单工具 `timeout_ms`,超时调用 `Tool::cancel()` 钩子并返回结构化 观测而不阻塞整个循环;需要真正中断的工具可实现 `run_with_cancel(args, should_stop)` 轮询停止谓词; 事件与历史顺序保持确定 - `AgentNode` 可直接作为图节点接入,支持 `history_channel` 续聊与 流式回调;未完成时向图引擎返回结构化 `error`;运行控制经 `NodeContext::control` 传入 Agent 循环 - MCP 客户端:`qa::mcp::Client` 完成握手、工具 / 资源 / 提示分页读取、 资源订阅与 `tools/call`;`add_mcp_tools()` 注册静态工具, `McpToolSync::sync()` 按服务端目录增量新增 / 更新 / 删除本对象管理的工具; `StdioTransport` 以子进程运行本地服务端, `HttpTransport` 通过 Streamable HTTP(JSON / SSE)连接远程服务端, 并按服务端能力订阅 GET SSE 流接收主动通知;断线以 `Last-Event-ID` 续传、 指数退避重连,工具 / 资源 / 提示的 `list_changed` 分别跟踪 - **RAG (`qa::rag`)** - `load_document()` 一站式加载:URL(HTML 自动剥离)/ 目录递归 / `.pdf` / `.docx` / `.html` / `.txt` / `.md` / `.jsonl` / `.csv` / `.log` - 路径统一按 UTF-8 处理(Windows 内部转 UTF-16):文档加载、checkpoint 与 JSON 索引支持中文 / 非 ASCII 路径;文件读写使用 `qa::fs::open_ifstream` / `open_ofstream` - `DocumentLoader` 抽象,PDF/DOCX/HTML/URL 提取器均可注入替换 - 滑窗切分(可选句子边界回退,UTF-8 码点对齐)、`Embedder` / `Chunker` 抽象;`OpenAIEmbedder` 支持分批请求与 429/5xx 退避重试 - 索引来源清单 `IndexManifest`(embedder / model / 维度 / chunker 签名), JSON 持久化携带;入库 / 替换前校验,模型不一致直接拒绝, 避免混用不同嵌入空间 - 向量库生命周期:按 id O(1) upsert、`remove` / `remove_document` / `replace_document`(原子替换单文档分块)、带 `ChunkFilter` 的过滤检索; 检索缓存向量范数并用 top-k 堆,只复制命中记录;内存实现线程安全, JSON 持久化使用“临时文件 + 替换”,加载失败不破坏现有索引 - 自定义持久化后端(SQLite / Postgres / FAISS / 远程存储等)通过子类化 `rag::VectorStore` 或覆写 `InMemoryVectorStore::save/load` 实现 - `ingest_documents()` 增量入库;`replace_documents()` 先完成切分与嵌入 校验,再按文档原子替换旧块,单个文档失败不影响其余文档; `VectorStore::replace_documents({DocumentChunks...})` 支持多文档批量替换, 内置实现整体校验后一次交换 - `UrlLoader` 默认执行 SSRF 防护(仅 http/https,拒绝控制字符 / 空白 / 反斜杠、回环 / 私有 / 链路本地地址,可用 `UrlPolicy` 放宽); 默认使用系统 DNS,校验每个解析结果后把连接固定到同一地址;重定向逐跳 复验、默认拒绝 HTTPS 降级,并要求实际 peer 与固定地址一致,同时检查 Content-Type 与实际响应体; 默认禁用系统代理,WinHTTP 在旧系统无法固定 HTTPS 地址时 fail-closed; `html_to_text()` 处理注释、脚本块、引号内标签与数字/命名实体 - `rag::LoadOptions` 统一 `metadata` / `html_parser` / `url_policy` / `extractor_timeout_ms` / `extractor_limits` / `max_source_bytes` / `max_extracted_bytes`;PDF / DOCX 内置抽取器通过 argv 直启,超时或输出 超限时清理整个进程树,并通过 Job Object / POSIX rlimit 限制资源; 这是资源隔离,不是完整文件系统/网络沙箱 - `Reranker` 重排策略接口,内置 `MMRReranker`,可换 cross-encoder / 规则 - 现成查询图:`retrieve -> build_context -> answer`;`AnswerNode` 支持 流式增量回答、结构化 sources、唯一 `[n]` 引用解析与多轮 `rag_history`;LLM 失败时节点返回结构化错误而非伪造 answer - 可运行服务核 `rag_service`:JSONL-over-stdio、持久化索引、 `health` / `ingest` / `query` / `shutdown`,可按会话保持多轮历史 ## 文档 | 文档 | 内容 | |---|---| | [开发手册](docs/DEVELOPMENT.md) | 环境、构建测试、执行模型、扩展点、并发、错误契约、CI 与发布 | | [接入手册](docs/INTEGRATION.md) | 安装导出、最小消费者、图/Agent/RAG 接入、配置密钥、部署排障 | | [API 速览](docs/API.md) | 公开类型与常用片段 | | [变更记录](CHANGELOG.md) | 版本变更与兼容性说明 | ## 目录结构 ``` quick_agent/ ├── CMakeLists.txt ├── cmake/quickagentConfig.cmake.in # install/export package config │ └── rag_service_smoke.cmake # CTest 离线服务冒烟脚本 ├── LICENSE # MIT ├── CHANGELOG.md ├── third_party/nlohmann/json.hpp # vendored ├── include/quickagent/ │ ├── QuickAgent.hpp # 总头文件 │ ├── export.hpp # QAGENT_API 导出宏 │ ├── core/ # state/node/retry/run_control/checkpoint/ │ │ # file_checkpoint_store/observer/graph/subgraph/stream/parallel(MapNode) │ ├── llm/ # message/mock_provider/http_client/json_schema/ │ │ # openai_provider/anthropic_provider │ ├── agent/ # tool.hpp / agent.hpp (ReAct, AgentObserver) │ ├── mcp/ # transport/client (JSON-RPC) / mock_transport / │ │ # http_transport / stdio_transport(opt-in,含 ) │ └── rag/ # document/loader/loader_factory/pdf_loader/web_loader/ │ # chunker/embedder/vector_store/reranker/pipeline ├── src/ # winhttp_client.cpp / curl_http_client.cpp / │ # openai_client.cpp / anthropic_client.cpp ├── examples/ # hello_graph / react_agent_demo / rag_demo / llm_live_check / │ # support_agent_demo / vector_bench / rag_service / │ # qt_http_client.example.cpp(参考代码,不进 CMake) │ └── data/ # support_agent_demo 的工单 / 历史案例 JSONL ├── tests/test_main.cpp # 断言式单元测试(128 cases) └── docs/ # DEVELOPMENT.md / INTEGRATION.md / API.md ``` ## 构建与测试 需要 C++14 编译器(MSVC 2022 / clang / gcc)与 CMake >= 3.16。 ```bash cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build ctest --test-dir build --output-on-failure ``` Windows 无独立 CMake 时,可在 VS2022 的 Developer PowerShell 中执行上述命令; 仓库根目录的 `build.bat` 也可直接复用。CMake 选项: | 选项 | 默认 | 说明 | |---|---|---| | `QAGENT_BUILD_TESTS` | `ON` | 构建单元测试 | | `QAGENT_BUILD_EXAMPLES` | `ON` | 构建示例 | | `QAGENT_INSTALL` | `ON` | 生成 install / `find_package` 导出规则 | | `QAGENT_HTTP_BACKEND` | `none` | HTTP 传输后端:`none`(不编译 HTTP 传输;Windows 仍链接系统 `ws2_32` 供 DNS 解析)/ `winhttp`(仅 Windows)/ `curl`(任意平台,触发 `find_package(CURL)`) | | `QAGENT_BUILD_SHARED_LIBS` | 跟随 `BUILD_SHARED_LIBS`(否则 `OFF`) | 构建动态库(默认静态库);共享构建自动设置符号导出与 PIC | Windows 上 `winhttp` 用系统组件、无需额外库;`curl` 后端需要自备 libcurl (系统不提供),获取方式见[开发手册 §2.1](docs/DEVELOPMENT.md#21-windows-上获取-libcurl)。 PDF / DOCX 加载测试依赖 Python(`pypdf` / `reportlab`)。测试会依次探测 `QAGENT_PYTHON`、`python3`、`python`、`py`,缺少可选依赖时对应用例以 `[SKIP]` 跳过,不影响其余用例;CI 在 Linux / macOS venv 与 Windows 上安装依赖后 运行。CI 还会执行 `cmake --install` 并构建 `tests/consumer`,通过 `find_package(quickagent 0.5 CONFIG REQUIRED)` 验证导出包可被下游工程消费。 ## 安装与集成 ```bash cmake --install build --prefix /path/to/prefix # 或: cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/path/to/prefix ``` 安装产物包含库文件(默认静态库,`QAGENT_BUILD_SHARED_LIBS=ON` 时为共享库)、 公开头文件、vendored 的 `nlohmann/json.hpp`、MIT LICENSE,以及 CMake 包配置。 下游工程: ```cmake find_package(quickagent 0.5 CONFIG REQUIRED) target_link_libraries(my_app PRIVATE quickagent::quickagent) ``` `add_subdirectory(quick_agent)` 亦可,目标别名同为 `quickagent::quickagent`。 当 `QAGENT_HTTP_BACKEND=curl` 时,包配置会自动 `find_dependency(CURL)`; 默认 `none` 模式不查找第三方网络库;Windows 目标仍会链接系统 `ws2_32`。 Windows 下共享构建会产出 DLL + import lib,运行时需将 DLL 放在 PATH 或与 可执行文件同目录。 ## 30 秒上手:图 ```cpp #include using namespace qa; core::GraphBuilder b; b.add_channel("text", core::Reducers::overwrite); b.add_node("upper", [](const core::NodeContext& ctx) { std::string t = ctx.get("text").get(); for (char& c : t) c = (char)std::toupper((unsigned char)c); return core::NodeResult::ok({{"text", t}}); }); b.add_edge("__start__", "upper"); b.add_edge("upper", "__end__"); core::State s; s["text"] = "hello"; auto r = b.build().run(s); // r.stop_reason == "finished"; r.final_state.get("text") == "HELLO" ``` 条件路由: ```cpp b.add_conditional_edges("classify", [](const core::State& s) { return s.get("parity").get(); }, {{"even", "even_node"}, {"odd", "odd_node"}}); ``` ### 错误契约 节点可以返回 `NodeResult::fail("...")` 或抛异常。引擎记录 `RunResult.errors`(节点名、消息、尝试次数),提交本批写入后以 `stop_reason == "error"` 停止,`final_state` 与检查点仍然可用: ```cpp auto r = b.build().run(s); if (r.stop_reason == "error") { for (const auto& e : r.errors) std::cerr << e.node << ": " << e.message << " (attempts=" << e.attempts << ")\n"; } ``` 需要重试时在节点上配置策略;默认情况下重试耗尽仍走上述错误契约, 只有显式 `rethrow_on_exhausted = true` 才向调用方抛异常: ```cpp b.set_retry("flaky", core::RetryPolicy{3, 100}); // 3 次尝试,线性退避 ``` ### 运行超时与取消 ```cpp // 运行级截止时间(毫秒);也可注入共享 RunControl 由外部 cancel() auto r = b.build().run(state, core::RunOptions::with_timeout(30'000)); if (r.stop_reason == "timeout") { /* 已提交检查点仍可 resume */ } auto control = std::make_shared(); b.add_node("scan", [](const core::NodeContext& ctx) { ctx.throw_if_stopped(); // 协作式停止点 return core::NodeResult::ok({{"done", true}}); }); // 另一个线程: control->cancel(); ``` `RunControl` 是协作式的:引擎在超级步之间、节点执行前后与重试退避中检查, 阻塞中的 HTTP/工具调用由各自超时兜底;停止后仍提交当前批次的检查点, `stop_reason` 为 `cancelled` 或 `timeout`,`RunResult.errors` 不记录该节点。 `MapNode` 的 worker 会检查停止信号,工具执行可用 `ToolExecutionPolicy::timeout_ms` 单独限时。 | `stop_reason` | 含义 | |---|---| | `finished` | 正常结束 | | `max_steps` | 达到步数上限 | | `halted` | 节点显式 `halt` | | `error` | 节点失败 / 路由错误 / 未知节点,详见 `RunResult.errors` | | `cancelled` | `RunControl::cancel()` | | `timeout` | 运行截止时间已到 | ## ReAct Agent ```cpp llm::MockProvider provider; // 换成 OpenAIProvider / AnthropicProvider 即上线 provider.queue_tool_call("c1", "calculator", {{"expression", "7*8"}}); provider.queue_text("7 times 8 equals 56."); agent::ToolRegistry tools; tools.add(std::make_shared()); agent::ReActAgent agent(provider, tools, "You are a careful assistant.", 5); auto result = agent.run("What is 7 multiplied by 8?"); // result.answer / result.events / result.usage / result.history // 失败时: result.finished == false, result.stop_reason / result.error ``` 工具并发与超时: ```cpp agent.set_tool_execution({/*parallel=*/true, /*max_concurrency=*/4, /*timeout_ms=*/10'000}); // 超时的工具返回 {"error": "...", "timeout": true} 观测; // 已运行的 worker 无法被强制终止,Agent 循环继续 ``` `ToolRegistry::execute()` 默认调用 `llm::JsonSchema` 校验 `parameters_schema()`。 参数不合法时不会执行工具,而是返回带 `tool` 与 `violations` 的错误观测;只有 可信且明确需要绕过校验的调用方才应使用 `set_validate_arguments(false)`。 接入真实模型: ```cpp // OpenAI 兼容端点 llm::OpenAIProvider client("https://api.openai.com/v1", api_key, "gpt-4o-mini"); // Anthropic Messages API(无需 Bearer;版本头默认 2023-06-01) llm::AnthropicProvider claude("https://api.anthropic.com/v1", anthropic_key, "claude-sonnet-4-5", nullptr, {}, /*default_max_tokens=*/1024); ``` 示例程序统一读取 `llm_config.json` 的 `"provider": "openai" | "anthropic"` 选择后端。Anthropic 不提供 embeddings 端点:配置 `embedding_model` 时需同时 提供 OpenAI 兼容的 `embedding_base_url` / `embedding_api_key`,否则示例回退 `HashEmbedder` 并打印警告。 流式与多轮: ```cpp auto first = agent.run_stream("List three uses of graphs.", [](const std::string& delta) { std::cout << delta << std::flush; }); auto second = agent.continue_with(first.history, "Elaborate on the second one."); ``` 流式能力直接来自 `LLMProvider::chat_stream`;不支持 SSE 的提供方会自动 降级为整段回调: ```cpp llm::ChatResponse resp; client.chat_stream(history, opts, [](const std::string& delta) { std::cout << delta; }, &resp); ``` ### 作为图节点 ```cpp auto node = std::make_shared( "chat", provider, tools, "You are a careful assistant.", /*max_iterations=*/5, /*query=*/"query", /*answer=*/"answer", /*history=*/"messages"); // 非空则按历史续聊 node->set_delta_callback([](const std::string& d) { std::cout << d; }); b.add_node(node); ``` `AgentNode` 写入 `answer`、`agent_history`、`agent_events`、`agent_usage`、 `agent_stop_reason`;循环未正常结束时向引擎返回 `error`。 ## RAG ```cpp rag::HashEmbedder embedder(256); // 离线确定性嵌入;上线换 OpenAIEmbedder rag::SlidingWindowChunker chunker(320, 60); rag::InMemoryVectorStore store; store.load("kb.json"); // 已有索引则读回 auto docs = rag::load_document("kb/"); // 目录递归 + 格式分发 rag::replace_documents(store, embedder, chunker, docs); // 校验 manifest + 嵌入,再替换旧块 store.save("kb.json"); // JSON 持久化(含 IndexManifest) // 自定义持久化后端可子类化 rag::VectorStore 或覆写 InMemoryVectorStore::save/load auto builder = rag::build_rag_query_graph(store, embedder, provider, "gpt-4o-mini"); core::State s; s["query"] = "检查点如何支持恢复?"; auto r = builder.build().run(s); // r.final_state["chunks"] / ["context"] / ["answer"] // / ["context_sources"] / ["answer_sources"] // / ["answer_cited"] / ["rag_history"] ``` `build_rag_query_graph` 返回 `GraphBuilder`,可继续追加节点/边/observer。 上线时用 `rag::OpenAIEmbedder(client, "text-embedding-3-small")` 替换 `HashEmbedder`。 ### 流式回答、引用与多轮 ```cpp rag::AnswerNode::Config acfg; acfg.model = "gpt-4o-mini"; acfg.max_tokens = 800; rag::RetrieveNode::Config rcfg; rcfg.k = 4; auto builder = rag::build_rag_query_graph( store, embedder, provider, "gpt-4o-mini", rcfg, nullptr, acfg, [](const std::string& delta) { std::cout << delta << std::flush; }); core::State s; s["query"] = "退款需要多久?"; s["rag_history"] = previous_history; // llm::Message JSON 数组 auto r = builder.build().run(s); // answer_sources: 实际被 [n] 引用的来源(结构化 id/doc_id/text/score) // answer_cited_indexes: 唯一且落在检索范围内的编号 // rag_history: 追加本轮 user/assistant 后的完整历史 ``` 编号上下文由 `BuildContextNode` 写入 `context_sources`;`AnswerNode` 从回答 中解析 `[n]`,去重并过滤越界编号。`history_channel` / `sources_channel` / `chunks_channel` 均可通过 `AnswerNode::Config` 改名。 ### 索引一致性 `ingest_documents` / `replace_documents` 会写入 `IndexManifest{embedder, model, dim, chunker}`。再次入库或服务重启时: - 清单一致才允许写入 / 加载; - embedder、模型、维度或 chunker 签名变化会直接报错,需显式重建索引; - 有数据但无清单的旧索引会被拒绝,避免静默混用不同嵌入空间; - 自定义 `Embedder` / `Chunker` 应实现 `id()` / `model()` / `signature()` 参与校验。 ### 文档加载 ```cpp auto pdf = rag::load_document("kb/manual.pdf"); // pypdf auto docx = rag::load_document("kb/report.docx"); // Python 标准库 auto html = rag::load_document("kb/page.html"); // 标签剥离 auto page = rag::load_document("https://example.com/doc"); // HTML 自动识别 ``` `load_document()` 按来源/扩展名分发,目录结果按路径排序,文档 id 使用绝对 规范化路径,避免同名文件互相覆盖。需要自定义解析时子类化 `DocumentLoader` 或给 `PdfFileLoader` / `HtmlFileLoader` / `DocxFileLoader` / `UrlLoader` 注入提取器。默认 URL 抓取走 `llm::HttpClient::get_ex()`,由 `UrlLoader` 自己逐跳处理重定向。`make_http_fetcher()` 也执行同样的逐跳、 peer 地址、Content-Type 和大小校验。默认使用系统 DNS,把校验过的地址固定给 传输层(Host/SNI 不变),并要求实际 peer 与该地址一致;同时禁用系统代理。 需要放宽策略时显式传入同一个 `UrlPolicy`。自定义 `UrlFetcher` 必须使用 传入的 `UrlResolution` 并设置 `dns_pinned`,且仍属于调用方信任的代码。 ### 向量库生命周期 ```cpp store.add(std::move(chunk)); // 非空 id 即 upsert store.remove("chunk-id"); store.remove_document("doc-id"); store.save("index.json"); // JSON 持久化 store.load("index.json"); // 校验失败不破坏现有索引 auto hits = store.search(query_vec, /*k=*/5, /*min_score=*/0.2, [](const rag::Chunk& c) { return c.metadata.value("lang", "") == "zh"; }); ``` 自定义后端只需实现 `rag::VectorStore`: ```cpp class MyVectorStore : public rag::VectorStore { public: void add(rag::Chunk chunk) override; // 写入你的索引 std::vector search( const std::vector& q, size_t k, double min_score) const override; // 仅返回相关度 top-k size_t size() const override; void clear() override; // remove/remove_document/save/load 按需覆写 }; ``` 把实例传给 `RetrieveNode` / `ingest_documents` / `replace_documents` / `build_rag_query_graph` 即可,图与 Agent 层无需改动。 ### 重排策略 向量库只返回相关度排序的原始邻居,去重 / 多样性 / cross-encoder 打分由独立的 `rag::Reranker` 策略负责: ```cpp rag::RetrieveNode::Config cfg; cfg.k = 4; cfg.reranker = std::make_shared(); // 或你的 CrossEncoderReranker cfg.rerank_lambda = 0.6; // 1 = 纯相关度 cfg.candidate_pool = 20; // 重排前候选池 ``` ### 检查点存储 ```cpp auto store = std::make_shared("run.json"); b.set_checkpoint_store(store); b.build().run(s); // 另一个进程:同一路径构造即可读回历史 core::JsonFileCheckpointStore again("run.json"); auto engine = b.build(); // 拓扑不变,reducers 会重挂 engine.resume(again.latest()); // 从最后一步继续 ``` 需要 SQLite / Postgres / 远程 KV 时,子类化 `core::CheckpointStore` (`save / list / latest / empty / size`)后同样经 `set_checkpoint_store` 注入; 线程作用域重载(`latest("alice")` 等)在基类里基于这五个方法实现,后端无需额外 工作。 `JsonFileCheckpointStore` 以 JSONL 追加写入(每次保存一行,不再重写整文件), 进程内并发保存由互斥锁串行化;旧版 JSON 数组文件首次保存时自动迁移,崩溃留下 的残缺末行会在加载时丢弃并在下次保存前修复。`erase_thread("alice")` 删除整个 会话的历史(自定义后端需覆写,默认抛出不支持),`compact()` 可手动压实文件。 多个进程可共享同一检查点文件:所有写入、重写、删除与裁剪都通过侧车 `*.lock` 文件(`qa::core::FileLock`)互斥,重写类操作会在锁内重新加载, 不会覆盖其他进程刚追加的记录。长期运行的任务可用保留策略清理历史: ```cpp core::JsonFileCheckpointStore store("run.jsonl"); // 每个会话只留最近 20 份 store.prune(/*keep_last=*/20); // 或按年龄 + 会话组合清理(max_age_ms 依赖 Checkpoint::created, // 该时间随检查点持久化) core::JsonFileCheckpointStore::PruneOptions opts; opts.thread = "alice"; opts.keep_last = 10; opts.max_age_ms = 24 * 60 * 60 * 1000; store.prune(opts); ``` ### 人工审批(HITL) ```cpp b.add_node("approve", [](const core::NodeContext& ctx) { if (!ctx.has_resume()) { ctx.interrupt(Json{{"question", "发布吗?"}, {"draft", ctx.get("draft")}}); } return core::NodeResult::ok({{"approved", ctx.resume()}}); }); auto r = b.build().run(s); // r.stop_reason == "interrupted",r.interrupts[0] 带 payload auto done = b.build().resume( r.checkpoints.latest(), core::ResumeCommand::for_node("approve", true)); ``` 暂停节点的写入不提交、节点留在 active 集;恢复会从节点开头重新执行,因此节点 需要幂等。细节见[开发手册 §5.3](docs/DEVELOPMENT.md)。 ### 图级流式输出 `run_stream()` 与 `run()` 语义一致(返回同一个 `RunResult`),额外把事件实时 推给宿主。节点在产生增量内容时调用 `ctx.emit(chunk)`: ```cpp b.add_node("write", [&](const core::NodeContext& ctx) { std::string answer; provider.chat_stream(history, options, [&](const std::string& d) { answer += d; ctx.emit(Json(d)); // 每个 token 立刻送达宿主 }); return core::NodeResult::ok({{"answer", answer}}); }); auto r = b.build().run_stream(s, [](const core::StreamEvent& e) { if (e.kind == core::StreamEventKind::Chunk) { std::cout << e.value.get(); // 流式渲染 } else if (e.kind == core::StreamEventKind::RunFinished) { std::cerr << "\n[" << e.reason << "] " << e.step << " steps\n"; } }); ``` 事件按发生顺序推送:`run_begin` → 每个节点的 `node_begin` / `chunk*` / `node_end` → 超级步 `step_committed` → …… → `run_finished`;暂停时额外有 `interrupt`。并发超级步的 `chunk` 来自多个工作线程,引擎用互斥量串行化回调, 因此回调内无需加锁,也不应回调同一引擎。`run()` / `resume()` 下 `ctx.emit()` 为空操作。 ### 子图(Subgraph) 把一段可复用的流程编译成图,再挂到父图上: ```cpp core::GraphBuilder retrieval; retrieval.add_channel("query", core::Reducers::overwrite); retrieval.add_node("search", [](const core::NodeContext& ctx) { return core::NodeResult::ok({{"docs", search(ctx.get("query"))}}); }); retrieval.add_edge("__start__", "search"); core::SubgraphConfig cfg; cfg.inputs = {{"query", "question"}}; // 子通道 <- 父通道 cfg.outputs = {"docs"}; // 子通道 -> 父通道 b.add_subgraph("retrieve", retrieval, cfg); b.add_edge("__start__", "retrieve"); b.add_edge("retrieve", "answer"); ``` - 空 `inputs` 复制全部父通道;空 `outputs` 回写全部子通道(append 归约通道 建议显式列出,避免历史重复)。 - 子图 `error` / `cancelled` / `timeout` / `max_steps` → 父节点失败; `halted` → 父图 halt。 - 子图暂停以 `{"subgraph": name, "interrupts": [...]}` 上抛;父图 `resume` 时把值交给 `SubgraphConfig::resume_node`(留空且只有一个待处理节点时自动 识别)。 - 子图 `ctx.emit()` 的 chunk 以 `{"subgraph", "node", "chunk"}` 转发到父流。 ### 线程 / 会话隔离 一次 `build()` 的引擎加一个存储即可服务多个会话: ```cpp auto store = std::make_shared("sessions.json"); b.set_checkpoint_store(store); auto engine = b.build(); core::RunOptions alice; alice.thread_id = "alice"; auto r1 = engine.run(state, alice); // r1.thread == "alice" core::RunOptions bob; bob.thread_id = "bob"; engine.run(other_state, bob); // 两条历史互不影响 // 按会话取快照;不传 thread_id 时恢复会沿用检查点的会话 auto cp = store->latest("alice"); auto done = engine.resume(cp, core::ResumeCommand::for_node("approve", true)); ``` - 检查点、`RunResult`、`StreamEvent`、`ctx.thread()` 都带会话标识;子图继承 父会话。 - `store->threads()` / `size("alice")` / `empty("bob")` 管理会话; `latest("x")` 在没有历史时抛 `std::out_of_range`。 - 不设置 `thread_id` 时与旧行为一致(会话标识为空)。 ### 结构化输出(JSON Schema) ```cpp const llm::JsonSchema schema(Json{ {"type", "object"}, {"properties", {{"name", {{"type", "string"}}}, {"age", {{"type", "integer"}}}}}, {"required", Json::array({"name"})}}); llm::ChatOptions opts; opts.model = "gpt-4o-mini"; auto out = llm::chat_structured(provider, history, opts, schema, 1); if (out.output.ok) { use(out.output.value); // 已通过本地校验 } else { for (const auto& v : out.output.violations) std::cerr << v.path << ": " << v.message << "\n"; // $.user.age } ``` - OpenAI 兼容端点会收到 `response_format: {type: "json_schema", ...}`; 其它 provider 忽略该字段,仍由本地校验 + 修复提示(`max_repair_rounds`) 保证结果。 - `llm::extract_json()` 处理 ```json 围栏与前后说明文字;`JsonSchema::accepts()` 可直接判断是否通过。 ### MCP 客户端 ```cpp #include // 可选;会拉入 mcp::StdioTransport::Options opts; opts.command = "npx"; opts.args = {"-y", "@modelcontextprotocol/server-filesystem", "/data"}; auto transport = std::make_shared(opts); auto client = std::make_shared(transport); client->initialize(); // 握手 + notifications/initialized agent::ToolRegistry tools; mcp::McpToolSync sync(tools, client, "fs_"); // 管理本服务注册的工具 sync.sync(); // 新增 / 更新 / 删除一次对齐 agent::ReActAgent agent(provider, tools, "You can use the filesystem tools."); ``` - `list_tools()` / `list_resources()` / `list_resource_templates()` / `list_prompts()` 自动跟随 `nextCursor` 并检测游标循环;`call_tool()` 返回 原始 `CallToolResult`,`McpTool` 会折叠成模型可读的文本 / 结构化观测。 - 资源与提示可通过 `resources()` / `read_resource(uri)` / `resource_templates()` / `prompts()` / `get_prompt(name, arguments)` 使用; 资源更新可调用 `subscribe_resource(uri)` / `unsubscribe_resource(uri)`。 - `McpToolSync::sync()` 只替换 / 删除自己注册的工具,外部同名工具会在改动 注册表前触发 collision 错误;需要长期热同步时应保留同一个 `McpToolSync` 实例。收到 `notifications/tools/list_changed` 后,在通知回调之外调用 `sync.sync()`(回调运行在接收线程,不能同步等待同一 client 的请求)。 - `add_mcp_tools()` 仍适合只注册一次的静态工具;`McpTool` 是注册时快照, 静态注册后不会自动更新 schema。 - 远程服务端用 `HttpTransport`(Streamable HTTP): ```cpp mcp::HttpTransport::Options http_opts; http_opts.url = "https://mcp.example/rpc"; http_opts.bearer_token = api_key; http_opts.headers = {{"X-Tenant", "acme"}}; auto client = std::make_shared( std::make_shared(http_opts)); // 或注入 Options::http client->initialize(); ``` 响应可为普通 JSON 或 SSE;会话 id、协议版本与关闭时的 DELETE 由传输层 自动处理。默认 HTTP 后端为 `none` 时需在 `Options::http` 注入自定义 `HttpClient`。 - 初始化完成后传输层按服务端能力打开 GET SSE 流(`HttpTransport::Options` 的 `subscribe`,默认开启;仅当能力含 tools/resources/prompts/logging 时 订阅,空能力视为未知仍订阅)。断线从 `reconnect_ms` 起指数退避到 `max_reconnect_ms`,并把最后收到的 SSE `id` 作为 `Last-Event-ID` 回传; 404/405/501 视为不支持。服务端主动通知经 `Client::set_notification_handler()` 送达;`tools_changed()` / `resources_changed()` / `prompts_changed()` 分别跟踪三类 `list_changed`,`clear_list_changed(kind)` 单独复位(`tools()` 自动 刷新工具缓存)。 - 自定义服务端接入只需实现 `mcp::Transport`(一行一条 JSON-RPC); `mcp::MockTransport` 用于离线测试。 - `StdioTransport` 丢弃子进程 stderr,关闭时先等待 1.5s 再终止。 ### 埋点与追踪 - 图层 `core::Observer`:`on_run_begin / on_node_begin / on_node_end / on_step_committed / on_retry / on_run_finished`, `GraphBuilder::add_observer(...)` 多播注册,内置 `LoggingObserver`、 线程安全的 `JsonTraceObserver` 与 `MetricsObserver`。 - Agent 层 `agent::AgentObserver`:`on_tool_call / on_final`, `ReActAgent::add_observer(...)` 或 `AgentNode::add_observer(...)` 注册。 ```cpp auto trace = std::make_shared(); auto metrics = std::make_shared(); b.add_observer(trace); b.add_observer(metrics); auto r = b.build().run(s); std::cout << trace->to_json().dump(2); // run_id / stop_reason / spans std::cout << metrics->to_json().dump(2); // steps / errors / retries / 节点耗时 ``` ## RAG 服务核 `rag_service` `examples/rag_service.cpp` 把持久化索引、流式回答、引用与会话历史包成 JSONL-over-stdio 进程,便于被 HTTP / WebSocket / gRPC 前端直接包装: ```bash ./build/rag_service --offline --docs examples/data --index rag_index.json ./build/rag_service --config llm_config.json --docs kb/ --index rag_index.json # 只读副本:共享写者生成的索引,令牌由参数或环境变量提供 ./build/rag_service --config llm_config.json --index /data/rag.json \ --read-only --auth-token "$RAG_TOKEN" ``` `llm_config.json` 的 `provider` 决定回答后端(`openai` / `anthropic`); Anthropic 未配置 `embedding_base_url` 时自动使用 `HashEmbedder` 并在 stderr 提示,不影响 JSONL stdout。 请求 / 响应均为单行 JSON(`--offline` 使用 `HashEmbedder`,回答为抽取式): ```jsonl {"id":1,"op":"health"} {"id":2,"op":"ingest","paths":["kb/"],"replace":false} {"id":3,"op":"query","query":"退款要多久?","session_id":"u1","stream":true} {"id":4,"op":"shutdown"} ``` `query` 先输出零到多条 `{"event":"delta","text":"..."}`,再输出一条 `{"event":"final", ...}`,包含 `answer` / `sources` / `cited_sources` / `cited_indexes` / `usage` / `stop_reason` / `run_id`。`session_id` 用于在进程内 保持 `rag_history` 多轮上下文(保留最近 40 条消息)。 - 启动时加载 `--index`;索引不存在且提供 `--docs` 时自动完成首次入库并落盘; - 索引 `IndexManifest` 与当前 embedder/chunker 不一致时拒绝启动; - `--auth-token`(或 `QAGENT_RAG_TOKEN`)非空时,每个请求必须携带 `"auth"` 字符串字段,比较使用常量时间实现,不匹配返回 `{"ok":false,"error":"unauthorized"}`; - `--read-only`(或 `QAGENT_RAG_READ_ONLY=1`)用于只读副本:拒绝 `ingest`、 必须复用已有索引,且与 `--docs` 互斥;`health` 返回 `auth_required` / `read_only` 便于探活; - `--timeout-ms` 映射为每次查询的 `RunOptions` 截止时间; - 所有响应都带请求 `id`;错误以 `{"ok":false,"error":"..."}` 返回,进程不退出。 CTest 中的 `rag_service_offline` 用例以管道输入 `health` / `query` / `shutdown` 验证协议与索引落盘;`rag_service_auth_readonly` 额外验证只读 + 鉴权副本、拒绝 ingest、索引文件字节不变与环境变量令牌路径。 ## 业务 demo:工单分诊闭环 `examples/support_agent_demo.cpp` 把图引擎、RAG 与 LLM 串成一条可运行的业务链路。 ### 业务流程图 ```mermaid flowchart TD subgraph INPUT["输入"] T["工单文件
support_tickets.jsonl
id / text"] C["历史案例
support_cases.jsonl
id / category / title / text / resolution"] CFG["LLM 配置
llm_config.json
provider / base_url / api_key / model"] end subgraph SETUP["初始化(main)"] CLI["解析 CLI
--offline / --top-k / --concurrency / --config"] MODE{"离线模式或
配置不可用?"} PROVIDER["provider == anthropic ?
AnthropicProvider : OpenAIProvider
HttpTimeouts + RetryConfig"] NOLLM["LLM = nullptr
关键词 / 模板模式"] EMB["选择 Embedder
embedding_model ?
OpenAIEmbedder : HashEmbedder"] EMBED_OK{"案例嵌入成功?"} INDEX["InMemoryVectorStore
案例索引就绪"] REINDEX["回退 HashEmbedder
重建索引"] end subgraph GRAPH["Graph 流水线"] LOAD["load
读取 / 校验 / 去重
无有效工单 → fail"] PREP["prepare
批量嵌入 query
写入 query_vec"] PROCESS["process · MapNode
按 --concurrency 并发"] PERSIST["persist
汇总 + 原子写 JSON
写失败 → fail"] end subgraph TICKET["单张工单(MapNode 内)"] SEARCH["RAG 检索
search(query_vec, top_k)"] PROMPT["构造 Prompt
工单 + 命中案例
问题 / 处理方案"] CHOICE{"LLM 调用成功且
JSON 字段有效?"} LLMRES["采用 LLM 结果
category / priority / reply"] FB["启发式兜底
关键词分类 + 优先级
案例模板回复 + note"] CITE["校验 citations
只保留检索命中的案例 id"] RESULT["组装结果
source / retrieved / usage"] end OUT[("support_results.json
mode / model / embedder
summary / tickets[]")] T --> CLI CFG --> MODE CLI --> MODE MODE -->|是| NOLLM --> EMB MODE -->|否| PROVIDER --> EMB C --> EMB EMB --> EMBED_OK EMBED_OK -->|是| INDEX EMBED_OK -->|否,远程嵌入异常| REINDEX --> INDEX INDEX -.->|top-k 检索| SEARCH PROVIDER -.->|chat| CHOICE CLI --> LOAD LOAD --> PREP --> PROCESS PROCESS -->|每张工单| SEARCH SEARCH --> PROMPT --> CHOICE CHOICE -->|是| LLMRES --> CITE CHOICE -->|否,超时 / 5xx / 非法 JSON| FB --> CITE CITE --> RESULT -->|全部完成| PERSIST PERSIST --> OUT ``` ### 单张工单时序 ```mermaid sequenceDiagram autonumber participant CLI as 用户 / CLI participant M as main participant G as GraphEngine participant S as VectorStore participant L as LLM Provider participant F as 结果文件 CLI->>M: 读取参数 + llm_config.json M->>S: 历史案例 embed + add M->>G: run() G->>G: load → prepare(批量 query 向量) loop 每张工单(并发) G->>S: search(query_vec, top_k) S-->>G: ScoredChunk[] alt LLM 可用且返回合法 JSON G->>L: chat(工单 + 历史案例) L-->>G: category / priority / reply / citations G->>G: citations 只保留命中 id else 超时 / 5xx / 非法 JSON G->>G: 关键词 + 案例模板兜底 end G->>G: 组装工单结果 end G->>G: persist 汇总 G->>F: 原子写(tmp + rename) F-->>CLI: [out] support_results.json ``` ### 失败与降级 | 场景 | 行为 | |---|---| | LLM 超时 / 429 / 5xx | 按 `RetryConfig` 退避重试;仍失败则该工单走兜底 | | LLM 返回非 JSON / 字段非法 | 丢弃 LLM 结果,保留关键词分类与案例模板回复 | | citations 引用未命中案例 | 过滤非法 id;为空时使用检索 top-2 | | 远程 Embedder 失败 | 切回 `HashEmbedder` 并重建案例索引 | | 工单行格式错误 / 重复 id | 跳过并告警;没有有效工单时 `load` 节点失败 | | 结果文件写入失败 | `persist` 节点返回结构化错误,运行以 `stop_reason=error` 结束 | ```bash ./build/support_agent_demo --offline # 无网络也可演示 ./build/support_agent_demo # 读取 llm_config.json 调真实模型 ./build/support_agent_demo --concurrency 2 # 慢端点降低并发 ./build/support_agent_demo --tickets my.jsonl --cases cases.jsonl --out out.json ``` - 输入:JSONL(也接受 JSON 数组)。工单字段 `id` / `text`;案例字段 `id` / `category` / `title` / `text` / `resolution`。格式错误行会跳过并告警。 - 检索:默认 `HashEmbedder`(离线字符 trigram);配置 `embedding_model` 后 改用 `OpenAIEmbedder`(Anthropic 场景需单独的 OpenAI 兼容嵌入端点)。 案例的 `resolution` 会作为引用上下文进入 prompt。 - LLM:要求返回 `{"category","priority","reply","citations"}`;只接受检索命中的 citation, 非法 JSON 或调用失败自动回退到关键词 + 模板,并在 `note` 中记录原因。 - 输出:每张工单包含 `category` / `priority` / `reply` / `citations` / `retrieved` / `usage`;顶层包含 `mode` / `model` / `embedder` / `summary`。 - `llm_config.json` 支持 `provider`(`openai` / `anthropic`)/ `base_url` / `api_key` / `model`,可选 `anthropic_version` / `embedding_model` / `embedding_base_url` / `embedding_api_key` / `temperature` / `max_tokens` / `json_mode` / `connect_timeout_ms` / `total_timeout_ms` / `retries` / `retry_backoff_ms`。Anthropic 无嵌入端点,需单独配置 OpenAI 兼容的 `embedding_base_url`,否则使用 `HashEmbedder`。 ## 可替换接口 | 接口 | 内置实现 | |---|---| | `core::CheckpointStore` | `CheckpointLog`(内存)、`JsonFileCheckpointStore` | | `core::Observer` | `LoggingObserver`、`JsonTraceObserver`、`MetricsObserver` | | `llm::LLMProvider` | `OpenAIProvider`、`AnthropicProvider`、`MockProvider` | | `llm::HttpClient` | 抽象接口;内置后端经 `QAGENT_HTTP_BACKEND=winhttp|curl` opt-in,默认 `none` 不编译 HTTP 传输(Windows 仍链接 `ws2_32` 供 URL DNS 解析),需注入自定义实现 | | `rag::DocumentLoader` | 文本 / 目录 / PDF / HTML / DOCX / URL | | `rag::Embedder` | `HashEmbedder`、`OpenAIEmbedder` | | `rag::Chunker` | `SlidingWindowChunker` | | `rag::VectorStore` | `InMemoryVectorStore`(暴力余弦检索 + JSON 文件持久化);持久化为接口,外部子类化实现 SQLite / Postgres / FAISS / 远程后端 | | `rag::Reranker` | `MMRReranker` | | `mcp::Transport` | `StdioTransport`(子进程 stdio)、`HttpTransport`(Streamable HTTP)、`MockTransport`(进程内) | ## 测试覆盖 128 个断言式用例,覆盖:归约器、条件分支、循环与检查点 resume、人工介入 (interrupt / resume)、图级流式事件 / 并发串行化 / 恢复流、 会话隔离 / 按线程过滤与跨进程重载、JSONL 检查点追加 / 迁移 / 残缺行 / 并发保存 / 删除会话 / 保留策略裁剪 / 侧车锁文件 / 双实例并发写、 子图映射 / 暂停恢复 / 多节点恢复与边界校验 / 错误与 halt、 JSON Schema 校验(含 `$ref` / 外部 `$ref` / `pattern` / `patternProperties` / `if/then/else` / 依赖关键字 / `format` / 排他边界)/ 宽松解析 / 修复循环 / 增量流式解析 / 线上请求体 / 取消短路、 MCP 握手 / 分页与游标循环保护 / 资源 / 资源模板 / 提示读取 / 工具桥接与 热同步 / 错误与超时 / 通知泵与三类 `list_changed` 缓存刷新 / Python stdio 端到端 / Streamable HTTP 会话与 SSE / GET 推送流断线续传 `Last-Event-ID` / 能力门控、 RAG 原子替换 / 批量多文档替换与校验回滚 / URL 字符 / DNS / 逐跳重定向 / DNS 地址固定 / 默认代理禁用 / peer / Content-Type / 响应大小策略 / HTML 实体 与注释 / 加载选项透传与解析器注入 / PDF / DOCX 子进程超时、输出上限与资源 限制、 自定义 CheckpointStore 镜像、重试成功/耗尽与错误契约、 并发超级步、MapNode 有界线程池、 运行截止时间 / 取消、未知节点错误契约、MetricsObserver / 重试事件、 ReAct 工具循环与错误观测、默认参数 schema 校验、并行工具执行与历史顺序、 工具超时 / 协作取消与 `cancel()` 钩子、历史预算裁剪、 图层 / Agent Observer 钩子与并发安全、 JsonTraceObserver span、流式降级 / SSE 解析 / usage / 非法工具参数、 Agent 流式与多轮 / history JSON 往返、文档加载与 loader factory、 嵌入确定性 / 分批请求 / 嵌入重试、向量 top-k / 与暴力扫描结果一致 / 范数缓存随增删改同步、向量库并发访问 / 加载原子性 / upsert/删除/过滤/替换、替换失败保护、索引 manifest 一致性、自定义 VectorStore、 自定义 Reranker / MMR 多样性、UTF-8 对齐切分、自定义 Chunker、 `InMemoryVectorStore` JSON 持久化往返 / 空 metadata 恢复 / 旧 JSON 行兼容、 HTTP 超时配置 / 请求取消选项、 RAG 查询图端到端 / 流式引用 / 多轮历史、Anthropic 请求形状 / 历史工具映射 / SSE 与 usage / 分片与多行事件 / 多工具块 / stop reason / 错误与重试 / URL 拼接、 `llm_runtime` 配置校验 / 默认值合并 / provider 选择 / 嵌入端点回退。 见 `tests/test_main.cpp`; CTest 另含 `support_agent_demo_offline`、`rag_service_offline`、 `ann_store_demo_offline` 与 `rag_service_auth_readonly` 四个示例冒烟用例。 ## 性能基线 `vector_bench`(Release)在本机的参考值: | 场景 | 结果 | | --- | --- | | 检索 50,000 × 128、top-10 | **10–14ms**(优化前约 53ms) | | 入库 50,000 × 128 | 30–38ms(约 1.3–1.6M chunks/s) | | `HashEmbedder` 1024d | 约 100–130 MB/s | 检索使用缓存范数 + top-k 堆,仅复制命中的 k 条记录。持久化为 `VectorStore::save/load` 接口:内置 `InMemoryVectorStore` 将索引与 `IndexManifest` 写成单个 JSON 文件(体积约为向量原始字节的数倍,属 I/O 受限, `vector_bench` 的 `persist` 行给出本机 save / 重载实测值);数据库 / 远程等 后端由外部子类化 `VectorStore` 实现。数值随机器与负载变化,仅用于同机回归对比。 ## 许可 [MIT](LICENSE)