数据截至 (上游 commit 136cd875b154)
可靠性、自动化与自托管
30 秒导读: 前四章讲的是 agent「怎么想、怎么答」的内核。这一章讲外圈:当你要把这个 agent 真的交给一群业务同事天天用,你需要的东西——先量准确率再上线(评测框架)、让 agent 从真实使用里自动改进(上下文推荐 + 自动提 PR)、让它定时自己跑(调度与自动化)、通过 Slack/Telegram 等渠道触达人,以及把这一整套自己部署起来(Docker + FastAPI 边车 + 双数据库 + 企业功能门控)。这些都不重复 01–04 的机制,而是围着它们建"生产化"的一圈。
本章不讲 agent 主循环、工具带、上下文文件系统、系统提示本身——那些在 02-agent-loop.md、03-tools.md、 01-context-filesystem.md、04-system-prompt.md。 这里只讲"把它们变成可信、可运维的产品"要多做哪些事。
1. 这是什么(零基础也能懂)
一句话: 一个内核很强的 analytics agent,离"敢给全公司用"还差五件外围工程——本章就是这五件。
想象你已经调好了一个能把自然语言变成 SQL、画图、讲故事的 agent。现在真要上线,老板会问你五个 很现实的问题:
| 老板的问题 | 本章对应的能力 | 白话 |
|---|---|---|
| "它答得准不准?上线前能证明吗?" | 评测框架 nao test | 拿一批「问题+标准答案 SQL」当单元测试,跑准确率 |
| "它会不会越用越好?" | 上下文自我改进 | 从真实报错/点踩里找出上下文缺口,甚至自动提 PR 修 |
| "能不能每天早上自己跑一份报表发我?" | 自动化与调度 | cron 定时触发 agent,把结果发邮件/Slack |
| "业务同事在哪用?" | 消息渠道集成 | Slack / Telegram / WhatsApp / Teams 里直接聊 |
| "数据敏感,能自己部署吗?" | 自托管栈 | 一个 Docker 镜像,自带双数据库、用你自己的 LLM key |
这正是 README 里那句 "Agent Reliability Visibility"(agent 可靠性可见性)+ "Self-hosted & secure" 想说的事:先看得见好坏,再敢部署。
一句话直觉/类比: 把内核 agent 当成一个刚招进来的分析师。本章做的是——先给他出一套考卷 (评测)、让他每周复盘自己答错的地方并改进笔记(上下文推荐)、给他排班表(调度)、 告诉他去哪个工位接活(渠道)、再给他一间上锁的独立办公室(自托管)。
2. 顶层全景(这一圈怎么转)
先看这五块外围能力各自"挂"在内核 agent 的哪个位置。怎么读这张图:中间竖线是内核 agent (前四章),两侧是本章的外围件,箭头方向 = 谁触发谁 / 数据往哪流。
┌───────────── 本章外围 ─────────────┐
评测(上线前) 内核 agent 自我改进(上线后)
┌───────────┐ ┌───────────────────────┐ ┌────────────────────┐
│ nao test │──提问─▶│ │◀─分析──│ 上下文推荐 run │
│ (Python) │ │ AgentService.generate │ │ (聚焦工具集) │
│ 期望SQL对比│◀─回答──│ = 主循环+工具带+提示 │──发现─▶│ → 自动提 PR │
└───────────┘ │ │ └────────────────────┘
└───────────┬───────────┘
调度/自动化 │ 渠道触达
┌───────────┐ 定时 payload │ 流式回答 ┌────────────────────┐
│ scheduler │───触发 automation─▶│──────────────────▶ │ Slack / Telegram │
│ (DB 队列) │ cron-nlp 生成 │ │ WhatsApp / Teams │
└───────────┘ │ └────────────────────┘
▼
┌────────────────────────────────┐
自托管栈 │ FastAPI 边车(execSQL/刷新) │ ← TS 后端 fetch 调用
(把上面全打包) │ SQLite / Postgres 双库 │
│ license 门控(sso/white-label) │
└────────────────────────────────┘
各部件一句话职责:
| 部件 | 干什么 | 关键文件 |
|---|---|---|
| 评测框架 | 跑「问题→期望 SQL」单测,量准确率/成本/耗时 | cli/nao_core/commands/test/、apps/backend/src/routes/test.ts |
| 上下文推荐 | 从真实使用信号找上下文缺口,写成建议 | services/context-recommendations.service.ts |
| 自动提 PR | 把建议的修改开成 GitHub PR | services/context-pr.service.ts |
| 调度器 | DB 支撑的定时任务队列(自动化、清理、推荐) | services/scheduler.service.ts |
| cron-nlp | 把"每天8点"翻译成 cron 表达式 | services/cron-nlp.ts |
| 渠道服务 | 把 IM 消息接进 agent、把回答发回去 | services/{slack,telegram,whatsapp,teams}.ts |
| FastAPI 边车 | 真正执行 SQL、定时 git pull 刷新上下文 | apps/backend/fastapi/main.py |
| 双数据库 | SQLite(单机)/Postgres(生产)自动切换 | apps/backend/src/db/dbConfig.ts |
| license 门控 | 校验签名 license,开关企业功能 | services/license.service.ts |
下面按"上线前 → 上线后 → 运维 → 部署"的顺序逐块深入。
3. 评测框架:上线前先把准确率量出来
3.1 它解决的小问题
analytics agent 最怕"看起来答得像模像样,数字其实是错的"。你需要在给用户前,拿一批已知正确 答案的问题去考它,量出通过率——像给代码写单元测试一样给 agent 写"单测"。
3.2 思路:问题 + 期望 SQL = 一个测试用例
一个测试就是一个 YAML 文件:一句自然语言 prompt,加一段"标准答案" sql。评测时不是去比
agent 生成的 SQL 文本(SQL 写法千变万化),而是:让 agent 自由回答 → 再用标准答案 SQL 跑出
"真值数据" → 把两边的结果表对齐比较。答案对不对看数 据,不看它怎么写的。
用例的数据结构很小(cli/nao_core/commands/test/case.py:11 TestCase):name / prompt / sql / file_path。discover_tests(case.py:34)扫 tests/ 目录下所有 *.yml/*.yaml
(TESTS_FOLDER = "tests/",case.py:8)。
一个最小用例长这样(示意):
# tests/01-top-customers.yaml
name: top-customers-2024
prompt: 2024 年销售额最高的 5 个客户是谁?
sql: |
SELECT customer_name, SUM(amount) AS total
FROM orders WHERE year = 2024
GROUP BY customer_name ORDER BY total DESC LIMIT 5
3.3 端到端走一遍:CLI ↔ 后端的分工
评测故意跨两端:Python CLI 负责调度和比较,TS 后端负责真正跑 agent。为什么?因为跑 agent 需要完整的模型/工具/上下文运行时,那套只在 TS 后端里有。
nao test (Python) TS 后端
──────────────── ──────────
discover_tests 读 tests/*.yaml
│
▼ 每个用例
run_test ──POST /api/test/run──────────▶ testAgentService.runTest
(client.py:113) {prompt, sql, model} (无痕跑一遍 agent,不落库)
│ │
│ 若带 sql:executeQuery 跑标准答案
│ runVerification 让 agent 把自己
│ 的答案整理成同样列的结构化数据
│◀──{text, toolCalls, usage,──────────────┘
│ cost, verification}
▼
check_dataframe 对齐比较 → ✓/✗
(runner.py:72)
- 后端侧(
routes/test.ts:35的/run):调testAgentService.runTest(services/test-agent.service.ts:41)——它复用主AgentService,但把 chat 标成testMode: true且不持久化,跑完就丢。如果用例带了sql,后端用 03-tools.md 里的executeQuery跑出期望数据(routes/test.ts:69),再调runVerification(test-agent.service.ts:69)让 agent 把它刚才的回答按指定列名重整成 结构化数据——这样两边才可比。 - CLI 侧(
runner.py:188run_test):打印 token/成本/耗时,再交给check_dataframe判定。
3.4 精华:比较是"结果等价"而非"字符相等"
check_dataframe(runner.py:75)是评测的灵魂。它做了一串归一化,让"实质相同但表面不同"的
结果判为通过:
| 归一化手段 | 解决什么"假差异" | 位置 |
|---|---|---|
| 数字字符串解析 | "1,234.5"、"€1.234,56" 各种千分位/币种/欧美格式 vs 裸数字 | compare.py:13 normalize_formatted_number |
| 浮点四舍五入到 2 位 | 3.14159 vs 3.14 的噪声差异 | runner.py:114 round_numeric |
| 列/行排序 | 列 顺序、行顺序不同但集合相同 | runner.py:128-139 |
np.allclose 近似 | 浮点尾差在容差内即算相等(rtol/atol) | runner.py:152-162 |
只有归一化后仍对不上,才判 ✗ 并打印 actual vs expected 差异表。跑完 save_results
(runner.py:286)把每条结果 + 汇总(通过数、总 token、总成本、平均工具调用数)写成
带时间戳的 JSON 存进 tests/outputs/。
nao test 还支持多模型对比(-m openai:gpt-4.1 -m anthropic:...)和多线程并发
(--threads,runner.py:552),让你横向比"哪个模型在你的数据上又准又便宜"。结果面板用
nao test server 起(cli/nao_core/commands/test/server.py)。
4. 上下文自我改进:从真实使用里学,还能自动提 PR
4.1 它解决的小问题
上线后,agent 一定会在某些问题上翻车:报错、被用户点踩、被反复重问。这些摩擦信号其实在 告诉你:上下文里缺了点东西(某张表没注释、某条业务规则没写清)。人工去翻聊天记录找规律很累。 上下文推荐就是让一个 agent 定期去做这件复盘,并给出可落地的修法。
这是本章工程含量最高的一支,也是"agent 改进 agent"的闭环。它是 beta 特性,由
BETA_CONTEXT_RECOMMENDATIONS_ENABLED开关(apps/backend/src/env.ts:196)。
4.2 思路:一个"戴着镣铐"的诊断 agent
runContextRecommendations(services/context-recommendations.service.ts:38)会新建一个内部 chat,
让主 agent 去分析一段时间窗内的使用数据。但这个分析 run 和普通聊天有两点关键不同:
- 换了系统提示——用
renderContextRecommendationsSystemPrompt(独立的诊断人格,components/ai/context-recommendations-system-prompt.tsx),让它当"上下文审计员"而不是"分析师"。 - 工具被严格收窄——
builtinToolAllowlist: ['read', 'grep', 'list', 'search'](context-recommendations.service.ts:121)。它只能读上下文、不能查数仓、不能画图。 这个 allowlist 机制在agents/tools/index.ts:119实现:凡不在名单里的内置工具一律丢弃, 只保留额外注入的专用工具(见下)。
为什么要收窄?因为诊断这件事只需要"看上下文文件 + 查应用库里的摩擦统计",给它数仓/画图工具反而 会分心、烧钱、跑偏。这是一个很值得学的模式:给聚焦任务配一套聚焦的工具集。
4.3 专用工具:记录发现、提议修复
分析 agent 手里换成了三类专用工具(注入进 getTools,context-recommendations.service.ts:109):
| 工具 | 作用 | 定义处 |
|---|---|---|
record_recommendation | 记一条"某文件某主题有问题"的发现 + 支撑证据 | agents/tools/record-recommendation.ts:94 |
edit_file | 对人写的上下文文件提议一处具体改动(存成 before/after) | agents/tools/propose-context-fix.ts:101 |
propose_manual_fix | 改不了的(目标是自动生成文件/上游源)→ 给一段可粘贴的修复 prompt | propose-context-fix.ts:147 |
工具不直接落库,而是把结果攒进内存 collector(createRecommendationCollector、
createContextFixCollector),run 结束后统一处理。edit_file 有一条硬规则:拒绝编辑
nao sync 自动生成的文件(如 databases/**),因为下次 sync 会覆盖掉——这种情况必须
改人写的源文件(RULES.md、semantics/**)或走 propose_manual_fix
(propose-context-fix.ts:203 resolveEditTarget 的报错逻辑)。
4.4 打分与去重:reconcile
一次 run 里 agent 可能记一堆发现,还可能和历史发现重叠。reconcile
(context-recommendations.reconcile.ts:153)负责把"本轮记录 + 历史记录 + 已解决 + 已忽略"合成
一组干净动作(insert / update / resolve):
- 去重靠指纹:
fingerprintFor(suggestedFile, subjectKey)(context-recommendations.reconcile.ts:129)对 "文件+主题"做 sha256,同一资源在一次 run 里只留最后一条,永不重复 insert。 - 弱信号被过滤:
computeImpact(context-recommendations.reconcile.ts:139)按"这条占了窗口内总摩擦 (错误+点踩+重生成)的多大份额 + 波及多少个 chat"算impactScore,低于IMPACT_FLOOR = 5(context-recommendations.service.ts:35)的直接不 insert。 - 旧问题重新出现会 reopen:已标 applied 或 snooze 到期的,再次被记录就重新打开
(
context-recommendations.reconcile.ts:182-184)。
分析 run 本身也有预算上限 ANALYSIS_STEP_BUDGET = 40(context-recommendations.service.ts:36),
防止诊断 agent 无限跑。
4.5 YOLO 模式:自动把修复开成 PR
如果项目开了 autoCreatePrs,run 结束前会调
autoCreateRecommendationPullRequests(context-pr.service.ts:123)把 impact 最高的几条
直接开成 GitHub PR 并标为 applied(数量上限 maxAutoPrsPerRun)。
这里的实现很 稳健,值得学——它绝不碰线上项目目录,而是在临时目录里克隆一份干净仓库来改
(context-pr.service.ts:164 createRecommendationPullRequest):
克隆到 mkdtemp 临时目录 → 新建分支 nao/context-<id>-<ts>
→ 把 proposedEdits 的新内容写进文件 → commit(带 nao 联合作者)
→ push → 调 GitHub API 开 PR → finally 删掉临时目录
写文件还加了两道安全锁,防路径穿越/符号链接逃逸:
assertInsideRoot(拒绝写到仓库外)、assertNoSymlinkInWritePath + writeFileAtomically
用 O_NOFOLLOW 等标志打开——这套锁已抽成独立工具 utils/safe-file-write.ts(apps/backend/src/utils/safe-file-write.ts:13-78)。单条 PR 失败只记日志跳过,不阻塞其余
(context-pr.service.ts:148)。
闭环全景: 用户报错/点踩 → 定时诊断 run 找出上下文缺口 → 提议修复 → 自动开 PR → 人 review 合并 → 下次
nao sync后 agent 用上更好的上下文。agent 帮你维护 agent 的上下文。