跳到主要内容

可靠性、自动化与自托管

30 秒导读: 前四章讲的是 agent「怎么想、怎么答」的内核。这一章讲外圈:当你要把这个 agent 真的交给一群业务同事天天用,你需要的东西——先量准确率再上线(评测框架)、让 agent 从真实使用里自动改进(上下文推荐 + 自动提 PR)、让它定时自己跑(调度与自动化)、通过 Slack/Telegram 等渠道触达人,以及把这一整套自己部署起来(Docker + FastAPI 边车 + 双数据库 + 企业功能门控)。这些都不重复 01–04 的机制,而是围着它们建"生产化"的一圈。

本章不讲 agent 主循环、工具带、上下文文件系统、系统提示本身——那些在 02-agent-loop.md03-tools.md01-context-filesystem.md04-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 PRservices/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_pathdiscover_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:19/run):调 testAgentService.runTest (services/test-agent.service.ts:23)——它复用主 AgentService,但把 chat 标成 testMode: true不持久化,跑完就丢。如果用例带了 sql,后端用 03-tools.md 里的 executeQuery 跑出期望数据(routes/test.ts:53),再调 runVerification(test-agent.service.ts:50)让 agent 把它刚才的回答按指定列名重整成 结构化数据——这样两边才可比。
  • CLI 侧(runner.py:181 run_test):打印 token/成本/耗时,再交给 check_dataframe 判定。

3.4 精华:比较是"结果等价"而非"字符相等"

check_dataframe(runner.py:72)是评测的灵魂。它做了一串归一化,让"实质相同但表面不同"的 结果判为通过:

归一化手段解决什么"假差异"位置
数字字符串解析"1,234.5""€1.234,56" 各种千分位/币种/欧美格式 vs 裸数字compare.py:13 normalize_formatted_number
浮点四舍五入到 2 位3.14159 vs 3.14 的噪声差异runner.py:107 round_numeric
列/行排序列顺序、行顺序不同但集合相同runner.py:121-132
np.allclose 近似浮点尾差在容差内即算相等(rtol/atol)runner.py:145-155

只有归一化后仍对不上,才判 ✗ 并打印 actual vs expected 差异表。跑完 save_results (runner.py:259)把每条结果 + 汇总(通过数、总 token、总成本、平均工具调用数)写成 带时间戳的 JSON 存进 tests/outputs/

nao test 还支持多模型对比(-m openai:gpt-4.1 -m anthropic:...)和多线程并发 (--threads,runner.py:416),让你横向比"哪个模型在你的数据上又准又便宜"。结果面板用 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:106)。

4.2 思路:一个"戴着镣铐"的诊断 agent

runContextRecommendations(services/context-recommendations.service.ts:35)会新建一个内部 chat, 让主 agent 去分析一段时间窗内的使用数据。但这个分析 run 和普通聊天有两点关键不同:

  1. 换了系统提示——用 renderContextRecommendationsSystemPrompt(独立的诊断人格, components/ai/context-recommendations-system-prompt.tsx),让它当"上下文审计员"而不是"分析师"。
  2. 工具被严格收窄——builtinToolAllowlist: ['read', 'grep', 'list', 'search'] (context-recommendations.service.ts:113)。它只能读上下文、不能查数仓、不能画图。 这个 allowlist 机制在 agents/tools/index.ts:71 实现:凡不在名单里的内置工具一律丢弃, 只保留额外注入的专用工具(见下)。

为什么要收窄?因为诊断这件事只需要"看上下文文件 + 查应用库里的摩擦统计",给它数仓/画图工具反而 会分心、烧钱、跑偏。这是一个很值得学的模式:给聚焦任务配一套聚焦的工具集

4.3 专用工具:记录发现、提议修复

分析 agent 手里换成了三类专用工具(注入进 getTools,context-recommendations.service.ts:101):

工具作用定义处
record_recommendation记一条"某文件某主题有问题"的发现 + 支撑证据agents/tools/record-recommendation.ts:43
edit_file人写的上下文文件提议一处具体改动(存成 before/after)agents/tools/propose-context-fix.ts:94
propose_manual_fix改不了的(目标是自动生成文件/上游源)→ 给一段可粘贴的修复 promptpropose-context-fix.ts:128

工具不直接落库,而是把结果攒进内存 collector(createRecommendationCollectorcreateContextFixCollector),run 结束后统一处理。edit_file 有一条硬规则:拒绝编辑 nao sync 自动生成的文件(如 databases/**),因为下次 sync 会覆盖掉——这种情况必须 改人写的源文件(RULES.mdsemantics/**)或走 propose_manual_fix (propose-context-fix.ts:184 resolveEditTarget 的报错逻辑)。

4.4 打分与去重:reconcile

一次 run 里 agent 可能记一堆发现,还可能和历史发现重叠。reconcile (context-recommendations.reconcile.ts:71)负责把"本轮记录 + 历史记录 + 已解决 + 已忽略"合成 一组干净动作(insert / update / resolve):

  • 去重靠指纹:fingerprintFor(suggestedFile, subjectKey)(reconcile.ts:47)对 "文件+主题"做 sha256,同一资源在一次 run 里只留最后一条,永不重复 insert。
  • 弱信号被过滤:computeImpact(reconcile.ts:57)按"这条占了窗口内总摩擦 (错误+点踩+重生成)的多大份额 + 波及多少个 chat"算 impactScore,低于 IMPACT_FLOOR = 5(context-recommendations.service.ts:32)的直接不 insert。
  • 旧问题重新出现会 reopen:已标 applied 或 snooze 到期的,再次被记录就重新打开 (reconcile.ts:99-104)。

分析 run 本身也有预算上限 ANALYSIS_STEP_BUDGET = 40(context-recommendations.service.ts:33), 防止诊断 agent 无限跑。

4.5 YOLO 模式:自动把修复开成 PR

如果项目开了 autoCreatePrs,run 结束前会调 autoCreateRecommendationPullRequests(context-pr.service.ts:53)把 impact 最高的几条 直接开成 GitHub PR 并标为 applied(数量上限 maxAutoPrsPerRun)。

这里的实现很稳健,值得学——它绝不碰线上项目目录,而是在临时目录里克隆一份干净仓库来改 (context-pr.service.ts:94 createRecommendationPullRequest):

克隆到 mkdtemp 临时目录 → 新建分支 nao/context-<id>-<ts>
→ 把 proposedEdits 的新内容写进文件 → commit(带 nao 联合作者)
→ push → 调 GitHub API 开 PR → finally 删掉临时目录

写文件还加了两道安全锁,防路径穿越/符号链接逃逸: assertInsideRepository(拒绝写到仓库外)、assertNoSymlinkInPath + writeFileNoFollowO_NOFOLLOW 打开(context-pr.service.ts:220-265)。单条 PR 失败只记日志跳过,不阻塞其余 (context-pr.service.ts:78)。

闭环全景: 用户报错/点踩 → 定时诊断 run 找出上下文缺口 → 提议修复 → 自动开 PR → 人 review 合并 → 下次 nao sync 后 agent 用上更好的上下文。agent 帮你维护 agent 的上下文。


5. 自动化与调度:让 agent 自己定时跑

5.1 它解决的小问题

"每天早上 8 点跑一份昨日销售报表,发到 Slack #data 频道"——这类需求要一个定时触发 agent 并把 结果送出去的机制。nao 有一套自建的、DB 支撑的调度器,以及"把大白话翻译成 cron"的小工具。

5.2 调度器:一张数据库表当任务队列

scheduler.service.ts 不依赖外部队列中间件,而是把 scheduled_job 表当队列,靠轮询 + 乐观 锁租约跑。这样"零外部依赖"是自托管友好的关键设计。

startScheduler (:69)
├─ 每 30s runPoll (:93) ──▶ claimDueJobs 抢一批到期任务
│ (queries:96,WHERE status='pending' 抢占)
│ ──▶ executeJob 逐个跑 (:119)
│ ├─ 成功 → 有 cron 就排下一次,否则删掉
│ └─ 失败 → onJobFailure 退避重试 (:151)
└─ 每 60s runReclaim (:108) ─▶ reclaimStaleJobs 回收崩溃任务
(租约 > 10min 的 running 重置为 pending)

关键设计点:

机制怎么做位置
防两个 worker 抢同一任务select 候选 → UPDATE ... WHERE status='pending' 乐观并发,不用 FOR UPDATE SKIP LOCKEDqueries/scheduled-job.queries.ts:96 claimDueJobs
崩溃任务不卡死租约超 10min 的 running 被回收成 pending,保留 attemptsscheduled-job.queries.ts:130 reclaimStaleJobs
失败退避30s→60s→2m→5m→10m 递增,超 maxAttempts 才放弃scheduler.service.ts:13 RETRY_BACKOFF_MS
重启不重置节奏ensureRecurring 幂等 upsert:重跑只刷新 cron,保留 runAt/attemptsscheduler.service.ts:41 / queries:22

启动时 startServer(app.ts:320-345)注册各类 job:日志清理(0 3 * * *)、MCP 查询数据清理 (0 4 * * *)、自动化、故事刷新、上下文推荐(beta 开关下),最后 startScheduler()。 这套乐观锁 + 相同语义在 SQLite 和 Postgres 上都成立——直接服务了自托管的双库需求。

5.3 大白话 → cron

用户不会写 cron。naturalLanguageToCron(services/cron-nlp.ts:9)用一次小模型调用把 "每周一早上 9 点"翻译成 0 9 * * 1,并cron-parser 校验能解析才返回,失败返回 null。 它在自动化(trpc/automation.routes.ts:198)和故事定时(trpc/story.routes.ts:271)里都用到。

配套还有一个 inferAutomationTitle(services/automation-title.ts:10):用小模型给自动化起个 3–8 词的短标题,失败就退化成从 prompt 截前几个词。这类"用一次便宜的 LLM 调用做小事"是全库的 共同习惯。

5.4 一次自动化 run 长什么样

自动化 = 定时触发的一次特殊 agent run。syncAutomationJob(automation.routes.ts:203)把 自动化写成 cron job(payload 带 automationId);到点后 automationHandler (handlers/automation.handler.ts:36)调 runAutomationfinishAutomationRun (automation.handler.ts:82)。

它和普通聊天的两个关键差别:

  1. 注入"送达"工具——createAutomationTools(services/automation-tools.ts:49)给 agent 加上 send_automation_emailsend_automation_slack_message、以及 GitHub 工具, 还有 get_automation_run_history(让它先看上次跑了啥、避免重复报告)。图表会渲成 PNG 附进 邮件、故事导成 PDF(automation-tools.ts:254)。
  2. 强制校验交付——邮件/Slack 等被启用的集成是 required 的 (automation-tools.ts:92 getAutomationIntegrationToolNames);run 结束时若 agent 没调 这些必需工具,直接判失败并报缺了哪个(automation.handler.ts:180)。这保证"定时任务不能 悄悄没发出去"。run 期间还流式落库中间状态,让人能中途打开看进度 (automation.handler.ts:210 drainAndPersistStream)。

6. 消息渠道集成:业务同事在哪用(简述)

除了 Web 聊天,nao 把同一个 agent 接进四个 IM 渠道,让业务同事在自己习惯的地方提问:

渠道服务Webhook 路由
Slackservices/slack.ts(+ slack-socket-bridge.ts Socket 模式)/api/webhooks/slack
Telegramservices/telegram.ts/api/webhooks/telegram
WhatsAppservices/whatsapp.ts/api/webhooks/whatsapp
Teamsservices/teams.ts/api/webhooks/teams

路由在 app.ts:184-197 注册;Slack 还在启动时对所有项目开 Socket 模式 (app.ts:352 slackService.startSocketModeForAllProjects)。

这些服务的形状高度一致(以 telegram.ts 为例):用 @chat-adapter/* 适配器把渠道消息转成 内部 UIMessage,调 agentService(还是同一个内核 agent),再把流式回答边生成边编辑回 渠道消息卡片(UPDATE_INTERVAL_MS = 200),图表渲成图片贴回去。共用工具在 utils/messaging-provider.ts要点:渠道只是 I/O 适配层,内核 agent 一份、复用到处。


7. 自托管栈:把上面这一整套自己部署

7.1 一个镜像跑两套运行时

nao 的一个反直觉之处:它同时需要 Node/Bun 和 Python。TS 后端跑 agent 主循环,但"真正连 数据库执行 SQL""解析 nao_config""定时 git pull 刷新上下文"这些是 Python CLI 的能力。所以生产 镜像里两者作为**边车(sidecar)**一起跑,由 supervisord 管理。

docker 容器
┌──────────────────────────────────────────────┐
│ supervisord (docker/supervisord.conf) │
│ ├─[program:backend] bun run cli.ts serve │ :5005 ← 用户/UI
│ │ │ fetch http://localhost:8005 │
│ │ ▼ /execute_sql │
│ └─[program:fastapi] uvicorn ...:app │ :8005 ← 只内网
│ (apps/backend/fastapi/main.py) │
└──────────────────────────────────────────────┘
TS 后端把 SQL 甩给 FastAPI 边车真正执行
  • 耦合点就是那一个 HTTP 调用:TS 的 SQL 工具 fetch localhost:${FASTAPI_PORT}/execute_sql (agents/tools/execute-sql.ts:25),FastAPI 侧 execute_sql(fastapi/main.py:222)加载 NaoConfig、选库、真正跑查询、把结果里 numpy/Decimal/NaN 等清成 JSON 安全类型返回。
  • 多阶段构建(Dockerfile):前端 Vite 构建、Python 用 uv 装 CLI、TS 后端不预编译 (Bun 直接跑 TS)。运行镜像还带 chromium(渲染图表/PDF)。
  • supervisord(docker/supervisord.conf):fastapibackend 两个 program 各自 autorestart=true,日志都转 stdout/stderr。
  • entrypoint(docker/entrypoint.sh)在起服务前先准备上下文,支持三种来源: local(挂卷)、git(启动时克隆/拉取,支持 PAT 和 SSH deploy key、sparse checkout)、 api(用 nao deploy 推送)。

7.2 FastAPI 边车还负责"定时刷新上下文"

除了执行 SQL,FastAPI 边车自己也有个调度器(注意:这和第 5 节 TS 那套是两套不同的调度器): 若设了 NAO_REFRESH_SCHEDULE,lifespan(fastapi/main.py:31)用 APSchedulerCronTrigger 注册 _refresh_context_task(main.py:64)——定时 provider.refresh()(git 源就 git pull),让容器里的上下文跟上仓库更新。还暴露 POST /api/refresh(main.py:190)供 CI/CD 或 webhook 主动触发。

谁刷新触发干的事
TS 调度器(第5节)DB 队列轮询自动化、清理、上下文推荐等业务 job
FastAPI 调度器NAO_REFRESH_SCHEDULE + APScheduler定时 git pull 刷新上下文文件

7.3 SQLite / Postgres 双库

同一份代码要能"单机 SQLite 起步 → 生产 Postgres 扩展"。切换只看 DB_URI 前缀 (db/dbConfig.ts:27 parseDbUri):postgres:// 走 Postgres,sqlite: 或裸路径走 SQLite。

dbConfig 据此选好 schema、迁移目录、连接串(dbConfig.ts:46),db.ts:14 createDb 再选 对应 drizzle 驱动。代价是两套 schema、两套迁移目录必须同步维护:

SQLitePostgres
schemadb/sqlite-schema.tsdb/pg-schema.ts
迁移apps/backend/migrations-sqlite/apps/backend/migrations-postgres/
抽象层db/abstractSchema.ts 让业务代码不关心方言同左

docker-compose.yml 演示了生产形态:一个 postgres:16 服务 + nao 服务,后者 DB_URI: postgres://... 指向前者。第 5 节调度器的乐观锁之所以刻意不用 FOR UPDATE SKIP LOCKED, 正是为了在这两个库上行为一致

7.4 许可与企业功能门控

nao 是 Apache-2.0 开源核,但有少数企业功能用签名 license 门控。机制是 hasFeature(feature)(services/license.service.ts:45):可用的 feature 只有两个—— ssowhite-label(types/license.ts:8 LICENSE_FEATURES)。

门控设计上很"防赖账",几点值得看:

  • license 是签名 JWT(EdDSA),用打包进镜像的公钥 jwtVerify 校验 (license.service.ts:246 verifySignedToken);篡改文件 → 签名校验失败 → feature 关闭。
  • 宽限期锚在 license 自己的 exp,不锚"上次成功校验时间"——所以重启或改数据库都无法 延长(license.service.ts:28 POST_EXPIRY_GRACE_MS,离线 license 无宽限)。
  • 在线心跳独立于第 5 节的通用调度器(startLicenseHeartbeat,license.service.ts:68), 每 12h 校验一次,防止"改 scheduled_job 表就关掉 license 校验";吊销是粘性的, 拿正常响应替换也不行——校验 token 里的 subscriptionId 必须匹配(license.service.ts:163)。
  • NAO_LICENSE = OSS 模式,企业 feature 全关,绝不联网。

生产镜像还在构建时用 docker/lock-license-key.mjs 把 license 公钥的开发覆盖分支剥掉 (Dockerfile:130),让 NAO_LICENSE_PUBLIC_KEY 环境变量在正式镜像里失效——防止有人换公钥伪造 license。


8. 巧妙之处(可借鉴的技术)

  • 评测比"结果"不比"SQL 文本",还做多层数字归一化(千分位/币种/浮点容差)——避开了"实质对 但字符不同"的假失败。runner.py:72compare.py:13
  • 给聚焦任务配聚焦工具集:上下文诊断 run 只给 read/grep/list/search,靠一行 allowlist 实现,省钱又不跑偏。context-recommendations.service.ts:113tools/index.ts:71
  • agent 改进 agent 的闭环:真实摩擦信号 → 指纹去重 + impact 打分 → 提议修复 → 自动 PR。 reconcile.ts:47/57context-pr.service.ts:53
  • 自动提 PR 绝不碰线上目录:临时克隆里改,O_NOFOLLOW + 路径穿越校验防逃逸。 context-pr.service.ts:94/257
  • 零外部依赖的调度器:一张表 + 乐观锁租约 + 退避重试 + 崩溃回收,SQLite/Postgres 通吃。 scheduler.service.ts:69scheduled-job.queries.ts:96
  • 自动化强制交付校验:required 工具没调就判 run 失败,定时任务不会"悄悄没发出去"。 automation.handler.ts:180
  • license 宽限期锚在签名 exp、心跳独立:重启/改库/黑洞域名都无法延长或伪造。 license.service.ts:28/68/163

9. 边界与局限(诚实)

  • 上下文推荐是 beta,默认关闭(env.ts:106BETA_CONTEXT_RECOMMENDATIONS_ENABLED); 自动 PR 需要连了 GitHub 且目标是"人写文件",改自动生成文件一律拒绝。
  • 评测的"验证"依赖模型二次整理答案(runVerification),不带 sql 的用例只跑不判、算 "no verification"(runner.py:231);比较对列名敏感(期望列缺失即判失败,runner.py:97)。
  • 双库是维护负担:每次 schema 变更要同时改 SQLite 和 Postgres 两套(见 nao 的 CLAUDE.md 提到 db:generate 遇到歧义会挂起,得退回 db:push)。
  • 调度器是单实例内轮询,靠乐观锁支持多副本,但不是重型分布式队列;30s 轮询意味着定时有 ~30s 抖动(POLL_INTERVAL_MS,scheduler.service.ts:9)。
  • 两套调度器并存(TS 业务 job vs FastAPI 上下文刷新),职责不同但概念上要分清,别混淆。
  • 企业 feature 目前只有两个(sso、white-label);门控只挡功能,不做用量计费。

10. 横向对比

本章讲的是"agent 的生产化外圈",在 ai-agent-reference 兄弟项目里,这一圈各有侧重:

  • 可评测性:nao 用"问题→期望 SQL,比结果表"这种数据领域特有的评测;通用 coding/agent 项目更多是"跑测试套件 / 人工 rubric"。nao 的归一化比较是它数据属性的直接产物。
  • 自我改进:nao 改的是上下文文件(声明式知识),不是改 agent 代码或 prompt 权重; 这与"记忆/反思型"agent(改的是长期记忆)是不同层次的"改进"。
  • 自托管:nao 的"TS 内核 + Python 边车 + 双库 + 签名 license"组合,反映它要同时讨好 "个人 pip install 单机跑"和"企业 Postgres + SSO 部署"两类用户。

要理解这些外围件服务的那个内核,回到: index.md(全景与阅读地图)、 02-agent-loop.md(被评测/被调度的那个主循环)、 03-tools.md(评测里跑 SQL、自动化里画图的工具带)、 01-context-filesystem.md(被诊断、被刷新的那份上下文)。


11. 代码地图(导航索引)

主题文件关键符号
评测:用例模型/发现cli/nao_core/commands/test/case.pyTestCasediscover_testsTESTS_FOLDER
评测:跑 + 比较cli/nao_core/commands/test/runner.pyrun_testcheck_dataframetest
评测:数字归一化cli/nao_core/commands/test/compare.pynormalize_formatted_numbernormalize_dataframe_numbers
评测:CLI→后端客户端cli/nao_core/commands/test/client.pyAgentClient.run_test/api/test/run
评测:后端服务apps/backend/src/services/test-agent.service.tsrunTestrunVerificationextractToolCalls
评测:后端路由apps/backend/src/routes/test.tstestRoutes/run
上下文推荐:主流程apps/backend/src/services/context-recommendations.service.tsrunContextRecommendationsANALYSIS_STEP_BUDGETbuiltinToolAllowlist
上下文推荐:去重打分apps/backend/src/services/context-recommendations.reconcile.tsreconcilefingerprintForcomputeImpact
上下文推荐:提议修复工具apps/backend/src/agents/tools/propose-context-fix.tscreateContextFixCollectorresolveEditTargetapplyEdit
上下文推荐:记录工具apps/backend/src/agents/tools/record-recommendation.tscreateRecommendationCollector
上下文推荐:自动 PRapps/backend/src/services/context-pr.service.tsautoCreateRecommendationPullRequestscreateRecommendationPullRequestwriteFileNoFollow
上下文推荐:调度 handlerapps/backend/src/handlers/context-recommendations.handler.tscontextRecommendationsHandlerensureContextRecommendationsSchedules
调度器apps/backend/src/services/scheduler.service.tsstartSchedulerrunPollexecuteJobonJobFailureensureRecurring
调度:任务队列查询apps/backend/src/queries/scheduled-job.queries.tsclaimDueJobsreclaimStaleJobsupsertRecurringJob
cron 自然语言apps/backend/src/services/cron-nlp.tsnaturalLanguageToCron
自动化:标题apps/backend/src/services/automation-title.tsinferAutomationTitle
自动化:送达工具apps/backend/src/services/automation-tools.tscreateAutomationToolsgetAutomationIntegrationToolNames
自动化:run handlerapps/backend/src/handlers/automation.handler.tsautomationHandlerfinishAutomationRun
自动化:tRPC 调度apps/backend/src/trpc/automation.routes.tsparseCronFromTextsyncAutomationJob
渠道:Telegram(样板)apps/backend/src/services/telegram.tsTelegramService
渠道:Slackapps/backend/src/services/slack.tsslack-socket-bridge.tsslackService
FastAPI 边车apps/backend/fastapi/main.pylifespan_refresh_context_taskexecute_sqlrefresh_context
TS→边车 SQL 调用apps/backend/src/agents/tools/execute-sql.tsexecuteQuery(fetch /execute_sql)
双库选择apps/backend/src/db/dbConfig.tsdb/db.tsparseDbUriDialectcreateDb
license 门控apps/backend/src/services/license.service.tshasFeaturestartLicenseHeartbeatrefreshLicenseOnline
license feature 定义apps/backend/src/types/license.tsLICENSE_FEATURES(ssowhite-label)
部署:镜像/编排Dockerfiledocker/supervisord.confdocker/entrypoint.shdocker-compose.yml多阶段构建、两 program、上下文来源三模式
启动装配apps/backend/src/app.tsstartServer(注册 job + startScheduler)