README.zh-CN.md / README.md 只留:这是什么、为什么用它、主要功能(按领域一行一条,链到文档)、快速开始、部署、文档索引、 开发与测试、许可(原文不动)。原来的每一节按读者和任务分到 17 篇主题文档:ask-data、workflows、evidence、 data-sources(新增:接入数据库、只读保证、上传表格、跨库合并、遮罩列)、data-catalog、tools(新增:工具、审批档位、 能力登记、端点、网络规则——它们管所有工作流,不只应用)、apps、evaluation-and-ledger、schedules(含通知)、deployment、 configuration、postgresql-and-scaling、identity-and-security、architecture、development、upgrade-notes(按时间倒序, 每波先写要做的事)、faq。删掉的只有模块名(api/copilot_gen.py、chat/drive.py、data/schema_view.py)、重复的话和 「以前…现在…」的叙述;操作步骤、命令、配置名、限制、权限边界都有归宿。中文是原文,英文按新的中文写;英文里界面文字 只写英文名,开头注明界面目前只有简体中文,中文占位符换成英文占位符,中文数字的例子改成不依赖中文的说法。 数据剖析可以同时剖析 2–3 张表(baf23998)收进 data-catalog。 新增 scripts/check-docs.py:英文文档(README.md、docs/en/**)不许有中日韩字符和全角符号(代码块也算), README、docs/**、deploy/README.md 的相对链接和锚点都要通,docs/zh-CN 与 docs/en 文件一一对应、标题层级相同。 中文提示里指向 README 章节的地方改指新文档:cluster/guard.py、cluster/version.py、db/base.py 两处报错, data/schema_view.py 文档串,.env.example 三处,scripts/prod.sh、scripts/test-postgres.sh,terms.ts 两处、 DataSourcesTab.tsx、check-manage.mjs 注释,deploy/README.md 三处链接。deploy/agentlab.env 未动。 验收:check-docs 0 问题(英文文档 18 个 0 处中文,相对链接 369 条 0 断链,18 对标题层级一致);相关后端测试 117 通过、7 跳过;tsc -b、check-copy 通过。
7.1 KiB
English | 简体中文
架构与设计决策
这篇讲 AgentLab 的总体架构,以及几个关键设计决策的理由。给想改代码或想理解系统为什么这样设计的人看。目录结构和开发流程见开发。
总体架构
flowchart TB
subgraph FE["前端 · React 19 + React Flow + Zustand"]
F1["画布编辑"]
F2["问数据"]
F3["运行追踪 · 证据面板"]
F4["decode.ts<br/><i>事件 → 人类可读的唯一翻译层</i>"]
end
subgraph BE["后端 · FastAPI + LangGraph"]
B1["engine<br/>编译 · 调度 · 中断恢复 · 出具与证据"]
B2["providers<br/>多模型接入与成本估算"]
B3["tools<br/>注册表 · MCP · 数据源"]
B4["memory<br/>记忆 · 知识库 · 混合检索"]
B5["sandbox<br/>microVM / Seatbelt / bubblewrap"]
end
subgraph ST["存储"]
S1[("agentlab.db 或 PostgreSQL<br/>业务数据")]
S2[("checkpoints.db 或同一个 PostgreSQL<br/>执行断点")]
S3[("artifacts/<br/>内容寻址工件库")]
end
FE <-->|REST + WebSocket| BE
B1 --> S1 & S2 & S3
受限动态编排
大多数实际任务的步骤是已知的:写周报、核对数据、审批后发布。让模型每次现场重新规划,既是在重复发明已知答案,也让结果不可复现——不可复现就没法验收。
所以 AgentLab 的引擎是编译式的:运行时模型改不了图结构,它的自由被结构性地限制在 agent 节点内部和分支决策里。在此基础上分三层:
flowchart LR
T["<b>模板层</b> · 骨架固定<br/><br/>已发布的不可变版本<br/>WorkflowVersion + 图哈希<br/>governed 级须过治理 lint"]
J["<b>关节层</b> · 有边界的自由<br/><br/>agent 工具白名单<br/>branch 语义分类<br/>缺数据时降档"]
E["<b>探索层</b> · 不进正式归档<br/><br/>画布试跑 / 临时图 / 追问<br/>跑通的路径提取为草稿"]
T ==>|约束| J
E -.人工审核后发布.-> T
- 模板层:正式运行只认已发布的不可变版本;受管级发布要过治理检查和证据门禁。
- 关节层:授权范围内的自由;工具白名单衰减报表把「已授权但从未使用」的工具显出来,为收缩权限提供依据。
- 探索层:结果标成探索性,不进正式归档;跑通的路径经「提取模板」沉淀为草稿,人工审核后发布。
怎么发布、怎么从探索走到模板,见工作流。
选用 LangGraph
可视化编排的难点不在画节点和连线,而在暂停、恢复与回放。LangGraph 的 checkpointer 和 interrupt() 把这几件事做在了底层:
- 人工介入是一次中断加一次
Command(resume=...),进程重启后仍可从断点继续。 aget_state_history()直接给出每一步的状态快照。
由此还有一项收益:agent 内部的工具审批不重复计费。interrupt() 会让整个节点重放,朴素实现会重复执行之前的模型调用。agent 节点里的模型调用和工具执行都包在 LangGraph 的 @task 里,结果进断点,重放时直接读缓存。
事件流是唯一的可视化数据源
节点通过 get_stream_writer() 发出事件,runner 把它们转成 RunEvent,落库并广播。画布高亮、token 流、工具卡片、时间线全部由同一份事件驱动:刷新页面或中途接入,都能拿到完整过程(WebSocket 先补历史,再接实时)。
前端对应地只有一个翻译层 run/decode.ts,画布右栏、问数据页、运行详情页共用它。分设几套的后果是:同一次运行在三个页面讲出三个版本,使用者没法判断哪个是真的。
画布动效只表示真实发生的事
画布上每个会动的东西都对应一件真实事件(含义见工作流)。跑一张十几个节点的图,一眼要能看出「谁在动」;纯装饰的动效做不到,只会让画布变吵。
- 协作矩阵和右栏泳道读的是同一份
reduceTeam(run/decode.ts),两处不会给出不同的并行度。 - 系统关掉动效时,光效整体不绘制,而不是停住:停在最后一帧的光弧和堆在起点的光点,看着像渲染坏了。
用护栏代替固定步数
固定步数想挡的是四件事:循环不收敛、花费越滚越大、上下文会满、真正失控。每件事都有更贴切的挡法(engine/guards.py):重复调用不再执行、令牌与金额预算、接近窗口先压缩再收尾,步数只留兜底。具体的值见工作流。
上限在运行发起时快照,恢复和接着跑沿用同一份:审批恢复时节点整个重放,上限一变,循环停的位置就和断点对不上。
沙箱分两档
轻量档用操作系统自带的访问控制(Seatbelt、bubblewrap),冷启动快,但 macOS 上限不住内存;重量档 microVM 有独立内核,内存限额真实生效,代价是第一次拉镜像要等。所以 auto 模式在镜像没缓存时先用轻量档。两档的保证、局限和实测数据见身份、权限与安全。
为什么检索是混合的
默认的 embedding 是本地特征哈希:不联网、不下载模型、零配置可用,但语义泛化能力弱。BM25 补上精确的关键词匹配。
实测(24 篇文档、16 个问题的评测集,hit@1):
| 配置 | hit@1 | 字面类 | 语义类 |
|---|---|---|---|
| 纯关键词(alpha=0) | 88% | 100% | 75% |
| 纯向量(本地哈希,alpha=1) | 81% | 88% | 75% |
| 混合 + Qwen3-Embedding-4B | 94% | 100% | 88% |
| 上述配置叠加模型重排 | 100% | — | — |
- 纯向量(81%)低于纯关键词(88%),混合才到 94%。专有名词靠 BM25,同义改写靠向量,两者各担一半——在这份语料上,混合检索是必要的,不是装饰。
- 样本只有 16 题,一题就是 6 个百分点。结论的方向可信,精度不要过度解读。
向量模型的维度靠实际探测,不按模型名推断。本地部署的模型维度各异(bge-m3 是 1024,Qwen3-Embedding-4B 是 2560);推断错了,后果不是偏差,而是全部存量向量被判定为和当前 embedder 不匹配,检索整体退回关键词。
为什么表格必须成为表
Excel / CSV 如果放进知识库,只能被切块检索,数字就成了模型从片段里「读」出来的。而本项目的基础约定是「所有算术下沉到 SQL 或口径卡」:产出复核能拦住凭空出现的数字,拦不住「从一堆片段里读错了一格」。所以表格上传后落成 SQLite 文件、注册成数据源(见数据源)。
连线走线
自动排版之后,层与层之间按「穿过这条走廊的连线,在竖直方向最多重叠几条」预留走廊宽度。前端的 canvas/routing.ts 据此给每条线分配独立的竖直车道,并错开端口、让跨层的长边从没有节点的空档绕过去。check-canvas-layout 用几何断言守这部分(见开发)。