2026年10月21日,看到这个消息我是震惊的,DeepSeek将内部推理框架DeepInfer开源了,梁圣NB,致敬梁圣我的小项目也顺便开源了吧
用 nginx + Drogon + gRPC + MySQL/Redis 搭一套经典的高并发后端骨架,在里面承载一个 可插拔的 Agent 运行时(Agent Harness),并给长期记忆接上真实的 语义召回(RAG)。
src/harness/:Agent 运行时。按 OpenHarness 思路拆成七个模块 (engine / tools / skills / permissions / hooks / memory / mcp),全部通过构造函数注入, 可单独替换、单独测试。模型热插拔由model_provider作为基础设施支撑。grpc-server:承载运行时的有状态后端(会话、长期记忆、审批待办都在这一侧)。gateway:无状态 HTTP 网关,只做参数校验、协议转换、状态码映射,可水平扩容; nginx 在前做轮询负载均衡。
| # | 亮点 | 说明 |
|---|---|---|
| 1 | 语义长期记忆 | 写入时用 BAAI/bge-m3(1024 维)生成向量,召回时按查询向量做余弦 top-k;无 key 自动回退"最近 N 条" |
| 2 | 七模块全部落地 | 并行工具真的并发、审批单真的落 Redis、上下文真的会压缩、MCP 真的能调外部工具 |
| 3 | 零 API key 可跑通全链路 | 内置 Fake LLM / Fake Embedding(规则驱动),make run-grpc-mock 即可复现全部分支 |
| 4 | 模型热插拔 | 接口 + 注册表;Fake 与 OpenAI 兼容实现按名注册,Agent 用 model 字段选型,业务零改动 |
| 5 | 回答可解释 | 响应带 status / confidence / model_used / activated_skills / context_tokens / context_compressed |
| 6 | 人在环是同步确认 | 返回 NEEDS_HUMAN + pending_id;调用方带 approval_id + approval_action 重试即可原地恢复执行 |
| 7 | 上下文工程 | token 估算 → 先历史压缩(LLM 生成摘要)→ 再裁剪,长会话不失控 |
┌──────────────────────────────────────────┐
客户端 / SDK │ gateway (Drogon, N 实例) │
│ │ AgentController → GrpcClient (stub) │
▼ └──────────────────┬───────────────────────┘
┌───────────┐ round-robin │ gRPC
│ nginx │ ──────────────►──────────┐ │
│ :8080 │ :8081 / :8082 │ │
└───────────┘ │ ▼
│ ┌──────────────────────────────────────┐
└►│ grpc-server (AgentServiceImpl) │
│ │ │
│ ▼ │
│ harness::Engine │
│ ├── ModelRegistry LLM/Embedding │
│ ├── ToolRegistry 内置工具+并发 │
│ ├── SkillManager 技能生命周期 │
│ ├── PermissionManager 守卫+审批 │
│ ├── HookRegistry 指标+审计 │
│ ├── LongTermMemory 语义召回(RAG) │
│ └── McpClient stdio JSON-RPC │
│ │ │
│ AgentRepository │
└──────┬───────────────┬───────────────┘
▼ ▼
┌─────────────┐ ┌─────────────┐
│ MySQL │ │ Redis │
│ 持久/连接池 │ │ 缓存 + hitl │
└─────────────┘ └─────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
DeepSeek /chat/completions SiliconFlow /embeddings
(LLM 生成) (BAAI/bge-m3 向量化)
分层职责
| 层 | 进程 | 职责 | 状态 |
|---|---|---|---|
| 接入层 | nginx :8080 | 反向代理、轮询负载均衡 | 无状态 |
| 网关层 | gateway :8081/:8082 | REST 校验、gRPC 调用、状态码映射、X-Gateway-Instance 标记 |
无状态,可水平扩容 |
| 运行时层 | grpc-server :9090 | Agent Loop 编排、会话/记忆/待办、向量索引 | 有状态 |
| 存储层 | MySQL / Redis | 持久化 + Cache-Aside 缓存 + 待办 TTL | — |
| 模块 | 组件 | 关键接口 | 实现要点 |
|---|---|---|---|
| ① 引擎 | harness/engine |
Engine::Run |
Agent Loop、上下文工程、工具编排、记忆抽取 |
| ② 工具 | harness/tools |
ToolRegistry::CallParallel |
风险元数据 + std::async 批次并发(默认 4) |
| ③ 技能 | harness/skills |
SkillManager::Match |
触发词 → 注入 prompt 片段 + 工具白名单;启停/版本/激活计数 |
| ④ 权限 | harness/permissions |
CheckInput/CheckOutput/CheckTool |
输入/输出守卫、风险分级、低置信度审批;审批单落 Redis 带 TTL |
| ⑤ 钩子 | harness/hooks |
HookRegistry::Run* |
OnBeforeChat/AfterChat/BeforeTool/AfterTool/OnError,旁路观察不阻断 |
| ⑥ 记忆 | harness/memory |
Save / Recall / RecallSemantic |
短期消息窗口 + 长期 MySQL 持久化 + 向量语义召回 |
| ⑦ MCP | harness/mcp |
Start / CallTool |
fork/exec 子进程,stdio 上的 JSON-RPC 2.0;mutex 串行化往返 |
harness/model_provider(LlmModel/EmbeddingModel/ModelRegistry)是引擎依赖的 基础设施,不计入七模块。
0) 审批重试? approval_id 非空 → Load 审批单 → approve 走 RunApproved / reject 直接熔断
1) hooks.OnBeforeChat
2) 输入守卫 PermissionManager::CheckInput(user_message)
3) 技能匹配 SkillManager::Match(user_message) → 计入激活统计
4) 长期记忆召回 LongTermMemory::RecallSemantic(agent_id, user_message, 5)
5) 选择模型 ModelRegistry::GetLlm(req.model) ← 热插拔点
6) 确定工具集 内置(Agent 白名单 > 技能工具并集 > 全部)+ MCP 外部工具
7) 短期记忆装载 history + 本轮 user
8) 多轮循环 RunLoop(≤ max_turns,默认 4)
├─ 上下文工程 BuildContext():system 组装 → 摘要压缩 → token 裁剪
├─ 模型 CompleteWithRetry()(指数退避)
├─ 无工具调用 → 输出守卫 → 人工介入判定 → 抽取长期记忆 → OK
└─ 有工具调用 → CheckTool(deny/高风险/低置信) → hooks → 执行
(内置并行 / MCP 串行)→ 回填 tool 消息 → 下一轮
9) 收敛失败 超过 max_turns → status=ERROR(防死循环)
10) hooks.OnAfterChat
长期记忆不是"把最近几条塞回去",而是按语义相关性召回。
写入:user_message
└─ Engine 调 Extract()(规则抽取 fact / preference)
└─ LongTermMemory::Save()
├─ EmbeddingModel::Embed(content) → 1024 维 float
├─ INSERT ... embedding=<hex 文本>, embedding_dim=1024
├─ 增量更新内存索引 index_[id]
└─ DEL memory:<agent_id>(失效读缓存)
召回:user_message
└─ LongTermMemory::RecallSemantic(agent_id, query, 5)
├─ EmbeddingModel::Embed(query) → 1024 维
├─ 在内存索引中筛出该 agent 的候选,算余弦相似度
├─ partial_sort 取 top-5
├─ 按 id 一次回表取原文(保持相似度顺序)
└─ 注入 system prompt("已知的长期记忆")
| 选择 | 理由 |
|---|---|
| MySQL 存向量 | 与业务数据同库同事务边界,便于备份/清理/排查;无需额外运维中间件 |
| LONGTEXT 存 hex | 纯 ASCII,拼 SQL 无需二进制转义;跨 MySQL/MariaDB/TiDB 行为一致;1024 维只占 8 KB |
| 进程内余弦 | 单机记忆量级(万级)下暴力检索足够快;启动时从 MySQL 重建,写入时增量更新 |
| 显式降级 | 没有 key、embedding 失败、索引为空、回表失败都会回到"最近 N 条",记忆功能永不失效 |
索引结构:std::map<int64_t, IndexEntry{memory_key, embedding}> + std::mutex。
读路径只锁"遍历 + 打分",网络调用和 DB 回表都在锁外。
升级接缝:把
index_与CosineSimilarity换成 Milvus / pgvector 即可,Save/RecallSemantic的对外语义不变。
CREATE TABLE long_term_memories (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
memory_key VARCHAR(128) NOT NULL, -- 归属维度(当前用 agent_id)
category VARCHAR(32), -- fact / preference
content TEXT,
created_at BIGINT NOT NULL,
embedding LONGTEXT NULL, -- float32 按内存字节序转 hex(1024 维 = 8192 字符)
embedding_dim INT NOT NULL DEFAULT 0, -- 0 表示尚未向量化
INDEX idx_memory_key (memory_key)
);EnsureSchema() 先 CREATE TABLE IF NOT EXISTS,再用 SHOW COLUMNS ... LIKE 探测
embedding / embedding_dim 是否存在,缺列才 ALTER TABLE。
这样不依赖 ADD COLUMN IF NOT EXISTS(MySQL 8 不支持、MariaDB/TiDB 支持),
老库升级与新库初始化走同一条幂等路径。
| 场景 | 行为 |
|---|---|
未配置 EMBEDDING_API_KEY |
vector_recall=off,RecallSemantic 直接调 Recall(最近 N 条) |
| 查询 embedding 请求失败 | 记日志并回退 Recall,本轮对话不受影响 |
| 写入时 embedding 请求失败 | 原文照常入库(embedding=NULL),只记日志,不阻塞记忆沉淀 |
| 启动时发现历史记忆没有向量 | LoadIndex() 逐条补向量并回写;失败的行下次启动再补 |
| 索引里没有该 agent 的候选 | 回退 Recall,避免"启用 RAG 后旧记忆彻底消失" |
启动日志会明确当前状态:
[memory] 语义索引已加载 26 条(embedding=BAAI/bge-m3,本次回填 26 条)
[grpc-server] 长期记忆语义召回=on embedding=BAAI/bge-m3
无 key 时:
[grpc-server] 长期记忆语义召回=off(回退最近N条) embedding=fake-embedding
# LLM:DeepSeek(OpenAI 兼容)
DEEPSEEK_API_KEY=sk-xxx DEEPSEEK_MODEL=deepseek-chat make run-grpc
# Embedding:SiliconFlow(OpenAI 兼容 /embeddings)
EMBEDDING_API_KEY=sk-xxx \
EMBEDDING_BASE_URL=https://api.siliconflow.cn/v1 \
EMBEDDING_MODEL=BAAI/bge-m3 \
EMBEDDING_DIM=1024 \
make run-grpc配置了真实 embedding 时,它会被注册为默认 embedding,长期记忆直接取默认模型;
未配置时默认是 fake-embedding,但语义召回开关保持关闭。
http_server_demo/
├── proto/agent/v1/agent.proto # 服务契约:Agent/Session/Chat/Tools/Skills/Memories/Stats
├── src/
│ ├── harness/ # ★ Agent 运行时(七模块)
│ │ ├── types.h # ChatMessage / ModelReply / ChatStatus / RiskLevel
│ │ ├── engine.* # Agent Loop + 上下文工程(Run / RunApproved / RunLoop)
│ │ ├── model_provider.* # 模型热插拔(Fake + OpenAI 兼容 + 注册表)
│ │ ├── tools.* # 内置工具 + 风险元数据 + 并行执行
│ │ ├── skills.* # 技能生命周期 + 触发词匹配
│ │ ├── permissions.* # 输入/输出守卫 + 工具权限 + 审批单
│ │ ├── hooks.* # 生命周期钩子(指标 + 审计日志)
│ │ ├── memory.* # 短期记忆 + 长期记忆 + 向量索引(RAG)
│ │ └── mcp.* # MCP 客户端(fork/exec + stdio JSON-RPC)
│ ├── grpc_server/ # main.cpp(组件装配)+ agent_service(RPC 实现)
│ ├── gateway/ # Drogon 控制器 + gRPC 客户端 + 单例上下文
│ ├── store/ # MysqlPool / RedisClient / AgentRepository / HttpClient
│ └── common/ # 命令行解析、JSON 工具
├── scripts/e2e_test.sh # 端到端冒烟(14 个场景,Fake 模型即可)
├── scripts/stress_test.sh # 压力测试(wrk/ab,分层场景 + 自动清理)
├── scripts/mcp_server.py # 示例 MCP Server(stdio JSON-RPC,零依赖)
├── deploy/nginx/nginx.conf # 轮询负载均衡配置
└── Makefile # proto / build / run-* / test / stress / lint / format
前置:MySQL(默认 127.0.0.1:3306,root 空密码,库 http_server_demo 自动创建)、
Redis(6379)、nginx。
make build # 生成 proto 代码 + cmake 编译
# 终端 1:运行时后端(Fake LLM + Fake Embedding,零 API key)
make run-grpc-mock
# 终端 2/3:两个无状态网关实例
make run-gateway-1
make run-gateway-2
# 终端 4:nginx(:8080 → 轮询 :8081/:8082)
make run-nginx
# 终端 5:一键冒烟(14 个场景)
make test体验一次:
curl -s -X POST http://127.0.0.1:8080/api/v1/agents \
-H 'Content-Type: application/json' \
-d '{"name":"助手","system_prompt":"你是一个简洁的中文助手。","tools":["get_time","calculator","get_weather","delete_record"]}'
curl -s -X POST http://127.0.0.1:8080/api/v1/sessions \
-H 'Content-Type: application/json' -d '{"agent_id":"1"}'
curl -s -X POST http://127.0.0.1:8080/api/v1/chat \
-H 'Content-Type: application/json' \
-d '{"session_id":"1","message":"现在几点,顺便算一下 2+3,再看下上海天气"}'接入真实模型与语义记忆:
DEEPSEEK_API_KEY=sk-xxx \
EMBEDDING_API_KEY=sk-xxx \
make run-grpc创建 Agent 时用 {"model":"deepseek-chat"} 选择真实 LLM;embedding 不需要在 Agent 上指定,
长期记忆统一使用注册表里的默认 embedding。
所有响应都带 X-Gateway-Instance 头(观察负载均衡轮询)。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/v1/agents |
注册 Agent:{name, system_prompt?, model?, tools?} → 201 |
| GET | /api/v1/agents |
列出 Agent |
| POST | /api/v1/sessions |
创建会话:{agent_id} → 201 |
| GET | /api/v1/sessions/{id} |
查看会话(含历史消息) |
| POST | /api/v1/chat |
★ 发起一次对话(完整 Agent 运行时链路) |
| GET | /api/v1/tools |
工具列表(描述中带风险等级) |
| GET | /api/v1/skills |
技能列表(触发词 / 工具 / 启用状态 / 版本 / 激活次数) |
| POST | /api/v1/skills/{name}/enable |
启用或停用技能:{"enabled":true} |
| GET | /api/v1/agents/{id}/memories |
查看某 Agent 的长期记忆(时间倒序,管理面) |
| GET | /api/v1/stats |
统计(Agent/会话/请求/工具调用/记忆/技能/熔断) |
| GET | /healthz |
健康检查(无 /api/v1 前缀):{status, instance, grpc_addr} |
请求:
{
"session_id": "1",
"message": "现在几点,顺便算一下 2+3",
"max_turns": 4, // 可选:覆盖运行时默认轮数
"approval_id": "hitl-...", // 可选:人工确认重试
"approval_action": "approve" // 可选:approve | reject
}响应:
{
"status": "OK", // OK | NEEDS_HUMAN | CIRCUIT_BREAK | ERROR
"reply": "我已根据工具返回的结果整理完成。",
"turns": 2,
"confidence": 0.95,
"model_used": "deepseek-chat",
"pending_id": "",
"reason": "",
"activated_skills": ["time_helper", "calculator_helper"],
"context_tokens": 86,
"context_compressed": false,
"tool_calls": [
{"name": "get_time", "arguments": "{}", "result": "{\"time\":\"2026-10-18 10:36:19\"}"},
{"name": "calculator", "arguments": "{\"expression\":\"2+3\"}", "result": "{\"expression\":\"2+3\",\"result\":5}"}
]
}| 状态码 | 场景 |
|---|---|
| 200 | 对话结束(OK / CIRCUIT_BREAK) |
| 201 | 创建 Agent / 会话成功 |
| 202 | NEEDS_HUMAN —— 需人工确认后带 approval_id 重试 |
| 400 | 缺必填字段(name / agent_id / session_id / message) |
| 404 | Agent / 会话 / 技能不存在 |
| 502 | gRPC 调用失败(后端不可用、超时、status=ERROR) |
Chat → 判定需要确认(高风险工具 / 置信度不足)
→ 生成审批单 → Redis `SET hitl:<id> ... EX 300`
→ 返回 202 + status=NEEDS_HUMAN + pending_id
调用方带 {approval_id, approval_action:"approve"} 重试
→ 取回审批单 → RunApproved → 继续执行剩余工具 → 正常返回
reject → 不计入统计地拒绝 → status=CIRCUIT_BREAK
审批单里保存了 action_json(待执行动作),所以恢复时不需要重放前几轮对话。
原始 message 由调用方在重试时带回;服务端用 approval_id 判断这是重试,
不会重复落 user 消息(is_approval 分支)。
| 判定点 | 规则 |
|---|---|
CheckInput |
命中 banned_keywords(如 drop table / ignore previous)→ kDeny |
CheckTool |
工具在 denied_tools → kDeny;RiskLevel::kHigh → kAsk |
CheckTool |
confidence < ask_confidence_threshold(默认 0.5)→ kAsk |
CheckOutput |
无回答且 confidence < min_confidence(默认 0.35)→ 熔断 |
CheckOutput |
finish_reason=content_filter → 熔断 |
熔断计数用 std::atomic<int64_t>,通过 GET /api/v1/stats 的 circuit_break_count 暴露。
system prompt(Agent 设定 / 全局默认 + 技能片段 + 长期记忆)
+ 历史消息(会话持久化)
+ 本轮 user
│
├─ 估算 token → 超过 compress_threshold(1200) 且消息数 > keep_recent(6)
│ → 用 LLM 生成摘要(summary 注入 system),替换较早消息
└─ 仍超过 max_context_tokens(1800)
→ 裁剪:从最老开始丢,直到 ≤ 上限或只剩 keep_recent(6) 条
trimmed / compressed / context_tokens 会随响应返回,便于观察。
| 层 | 内容 | 生命周期 |
|---|---|---|
MySQL agents |
Agent 配置(name/system_prompt/model/tools) | 永久 |
MySQL sessions / messages / tool_calls |
会话元数据、对话历史、工具调用记录 | 永久 |
MySQL long_term_memories |
长期记忆 + 向量(hex) | 永久 |
Redis memory:<agent_id> |
长期记忆读缓存(JSON 数组) | 30s TTL,写后失效 |
Redis hitl:<pending_id> |
审批单 | 300s TTL |
读写策略是 Cache-Aside:召回先查 Redis,未命中回源 MySQL 并回填;
Save 成功后 DEL memory:<agent_id>。语义召回本身走内存索引,不依赖 Redis。
McpClient 用 fork/exec 拉起 python3 scripts/mcp_server.py,
通过 stdin/stdout 上的 JSON-RPC 2.0(一行一条消息)完成:
initialize握手tools/list发现工具(示例服务提供 3 个)tools/call调用,取content[].text作为结果
stdio 是单通道,多个 gRPC 线程并发调用时用 mutex 串行化"写请求 → 读响应",
避免响应交错。MCP 启动失败只会降级为"仅内置工具",不影响服务启动。
- 无状态网关:gateway 不保存任何会话状态,可随意加实例;nginx 轮询。
- 连接池:MySQL 池默认 8 条,
acquire()返回 RAII guard,异常路径也能归还。 - 并行工具:
ToolRegistry::CallParallel用std::async分批(默认 4), 结果顺序与入参严格一致。 - 锁的粒度:注册表锁只保护"查找/复制",不保护工具调用本身; 长期记忆的索引锁只覆盖遍历打分,embedding 网络调用和回表都在锁外。
- 防死循环:
max_turns(默认 4)兜底,超轮直接ERROR。 - 失败可降级:MCP 失败 → 仅内置工具;embedding 失败 → 仅原文入库; Redis 不可用 → 记忆召回回源 MySQL(缓存层只是加速)。
压测入口是 scripts/stress_test.sh(make stress),链路为
wrk → nginx:8080 → gateway:8081/8082 → grpc_server:9090 → MySQL/Redis。
脚本优先用 wrk(Lua 构造 POST、自带 p50/p99 直方图),未安装时自动回退到 ab。
环境:Apple M2 Max(12 核)/ macOS 26.6.2 / wrk 4.2.0;grpc_server 与两个 gateway 同机运行,MySQL/Redis 本机,Fake LLM(无外部网络)。每组独立运行 10s(chat 5s), chat 场景预建 32 个会话轮转,避免单会话历史无限增长。
| 场景 | 并发 | QPS | p50 | p99 | 错误 |
|---|---|---|---|---|---|
GET /healthz(仅网关) |
10 | 55,956 | 0.14 ms | 0.22 ms | 0 |
| 50 | 60,537 | 0.79 ms | 0.98 ms | 0 | |
| 100 | 60,742 | 1.63 ms | 1.96 ms | 0 | |
| 200 | 59,350 | 3.36 ms | 5.06 ms | 0 | |
GET /api/v1/agents(Redis 缓存池) |
10 | 9,284 | 0.84 ms | 1.45 ms | 0 |
| 50 | 10,809 | 3.91 ms | 14.10 ms | 0 | |
| 100 | 11,024 | 8.48 ms | 30.64 ms | 0 | |
| 200 | 11,198 | 16.27 ms | 60.30 ms | 0 | |
GET /api/v1/tools(进程内) |
50 | 19,732 | 1.66 ms | 97.24 ms | 0 |
GET /api/v1/stats(Redis 计数) |
10 | 9,783 | 0.80 ms | 1.35 ms | 0 |
| 50 | 10,891 | 3.76 ms | 15.69 ms | 0 | |
| 100 | 10,868 | 8.59 ms | 25.86 ms | 0 | |
| 200 | 10,810 | 17.59 ms | 51.47 ms | 0 | |
POST /api/v1/chat(完整链路) |
10 | 997 | 7.58 ms | 113.62 ms | 0 |
| 20 | 957 | 17.33 ms | 189.01 ms | 0 | |
| 50 | 1,004 | 41.00 ms | 251.81 ms | 0 |
表中
agents/stats是 Redis 连接池改造后(--redis-pool-size=8)的复测值;healthz/tools不经过 Redis,沿用同机基线。chat受后台负载影响较大, 多次复测在 0.96k–1.2k 之间波动,表中取同轮稳定值。
读法:
- healthz 约 60k QPS 后走平,延迟随并发线性上升,说明入口(nginx/wrk 客户端) 已饱和;这一层不含存储访问,可作为链路基准。
- agents/stats 从改造前的 ~8k 提升到 ~11k(约 +38%):
RedisClient已从 "单连接 + 全局 mutex"改为 8 条连接的池(--redis-pool-size,默认 8), 互斥锁只保护借/还两个 O(1) 动作,网络往返真正并行。对照实验(同一时刻、 同机、c=50):--redis-pool-size=1时 agents=7.7k / stats=8.2k,复现旧上限; pool=8 时为 10.8k / 10.9k。 - QPS 在 c=100 后仍走平(~11k):瓶颈已从 Redis 转移到网关/gRPC 转发层; tools(纯进程内、同样走网关)能到 ~20k,可作为该层上限的参考。
- chat 稳态约 1.0k QPS:每个请求要读会话历史、写 user/assistant 消息、召回长期记忆、
更新统计,且共享一个
Engine单例。pool=1 与 pool=8 的 chat 数据一致(约 0.96k), 说明 Redis 池化不是 chat 的瓶颈;p99 明显高于 p50,主要来自 MySQL 连接池(8) 排队与会话历史增长。真实模型模式下 LLM 网络时延会完全主导,这组数据只代表 "Fake 模型 + 本地存储" 的链路开销。
压测会写真实数据:chat 场景自动创建
stress-agent-*Agent/会话/消息, 结束后由脚本trap EXIT删除;异常退出可用make stress-clean兜底,KEEP_DATA=1可保留现场。/api/v1/stats的累计计数(requests/tool_calls) 会包含压测增量,不会回滚。
make stress # healthz + agents + chat(c=50, 10s)
make stress-quick # healthz + agents(c=20, 5s)
make stress SCENARIOS=agents CONCURRENCY=100 DURATION=15s
make stress-clean # 清理上次压测遗留数据| 参数 | 默认值 | 说明 |
|---|---|---|
--port |
9090 |
gRPC 监听端口 |
--mysql-host/port/user/password/database |
127.0.0.1/3306/root/空/http_server_demo |
MySQL 连接池 |
--mysql-pool-size |
8 |
连接池大小 |
--redis-host/port |
127.0.0.1/6379 |
Redis |
--redis-pool-size |
8 |
Redis 连接池大小(每条连接同一时刻只归一个线程) |
--llm-base-url |
DEEPSEEK_BASE_URL 或 https://api.deepseek.com |
OpenAI 兼容入口 |
--llm-api-key |
DEEPSEEK_API_KEY |
为空则不注册真实 LLM |
--llm-model |
deepseek-chat |
注册名 |
--embedding-base-url |
EMBEDDING_BASE_URL 或 https://api.siliconflow.cn/v1 |
OpenAI 兼容 /embeddings |
--embedding-api-key |
EMBEDDING_API_KEY |
为空则语义召回关闭 |
--embedding-model |
BAAI/bge-m3 |
向量模型名 |
--embedding-dim |
1024 |
向量维度 |
--max-turns |
4 |
工具循环上限 |
--tool-concurrency |
4 |
并行工具批次大小 |
--context-max-tokens |
1800 |
上下文硬上限 |
--context-compress-tokens |
1200 |
触发历史压缩的阈值 |
--context-keep-recent |
6 |
裁剪时保留的最近消息数 |
--hitl-confidence |
0.5 |
低于该置信度需人工确认 |
--hitl-ttl |
300 |
审批单 TTL(秒) |
--breaker-min-confidence |
0.35 |
低于该置信度触发熔断 |
--mcp-script |
scripts/mcp_server.py |
MCP 服务脚本(传空串 = 禁用 MCP) |
--mcp-cmd |
python3 |
MCP 子进程可执行程序 |
--mcp-timeout-ms |
5000 |
单次 MCP 请求超时(毫秒) |
--llm-timeout |
60 |
LLM 请求超时(秒) |
--llm-max-retries |
1 |
LLM 失败重试次数 |
--llm-retry-backoff-ms |
300 |
首次退避毫秒数(指数退避) |
--system-prompt-file |
空 | 全局默认 system prompt 文件路径 |
make proto # 只重新生成 gRPC/protobuf 代码
make build # proto + cmake 编译
make run-grpc # 真实 LLM/Embedding(读环境变量)
make run-grpc-mock # Fake 模式,零外部依赖
make test # 端到端冒烟(需要各进程已启动)
make stress # 压力测试(wrk 优先、ab 兜底;chat 数据自动清理)
make stress-quick # 快速压测(healthz + agents,20 并发 / 5 秒)
make stress-clean # 清理上次压测遗留的 Agent/会话/消息
make lint # clang-format 检查(Google 风格)
make format # 自动格式化
make clean # 清理构建产物- 向量索引在进程内存中,多实例部署时需要各自加载(或后续接 Milvus)。
- 记忆抽取是规则驱动(
Extract匹配"我叫/我喜欢/记住"等),不是 LLM 抽取。 - 人在环是同步阻塞语义:调用方必须带
pending_id重试,暂不支持服务端回调。 - MCP 的 stdio 通道用 mutex 串行化,外部工具调用无法并行。
- Fake LLM 只用于打通链路,回答是固定模板,不代表真实模型质量。
- 网关没有鉴权/租户隔离,默认假设部署在内网;生产需自行加认证与限流。