# AgentLab **可视化 Agent 编排实验台**——在画布上定义工作流,运行时逐节点观察执行过程, 并对产出施加可验证的约束。 面向通用场景的**受限动态编排**:模板定义骨架,agent 只在被授权的关节处自由。 所有算术下沉到受控指标集,叙述层无权创造数字——报告里的数字由系统照快照填值, 每一个都点得开出处;正式运行钉死在不可变版本上,每一次执行的完整证据落进内容寻址的工件库。 ![运行中的画布](docs/images/canvas-running.png) 研究助手模板用 Mock 执行中:跑完的节点打勾,执行中的节点显示耗时和流式输出; 底部航迹是实时时间轴,右栏是同一份事件流驱动的运行步骤。 --- ## 目录 - [快速开始](#快速开始) · [生产部署](#生产部署) · [核心能力](#核心能力) · [受限动态编排](#受限动态编排模板定骨架agent-填关节) - [可点击证据](#可点击证据报告里的每个数都点得开出处) · [数据目录](#数据目录让助手和-agent-看懂业务数据) · [工作流形态](#工作流形态) · [节点类型](#节点类型) · [架构](#架构) - [关键设计决策](#关键设计决策) · [运行环境](#运行环境) · [常见问题](#常见问题) · [开发与验证](#开发与验证) --- ## 快速开始 环境要求:conda([miniforge](https://conda-forge.org/miniforge/) 即可)、Node 20+、pnpm。 **无需 Docker**——代码沙箱基于操作系统自带的隔离原语,可选升级至 microVM。 ```bash conda env create -f environment.yml # 首次:创建 agentlab 环境并安装依赖 ./scripts/dev.sh # 之后:一条命令启动前后端 ``` `dev.sh` 自动定位 `agentlab` conda 环境;环境不存在时依 `environment.yml` 创建。 如需使用其他环境名,设置 `AGENTLAB_CONDA_ENV`。 后端依赖的版本以 `backend/requirements.lock` 为准,装到的是测试跑过的那一套, 不是当天的最新版(见[依赖与锁文件](#依赖与锁文件))。 服务启动于 http://localhost:5273 。 要让同一网络里的手机、别的电脑打开,用 `./scripts/dev.sh --host`:前端监听所有网卡,启动时打印局域网地址; 后端仍只听 127.0.0.1,页面里的 `/api` 由前端开发服务器在本机转发。**AgentLab 没有登录**,同一网络里打得开 页面的人都能用它——跑工作流、花你配的模型额度、读数据源、执行沙箱代码,只在信得过的网络里开。 认不出的参数直接报错,`./scripts/dev.sh --help` 看用法。 **无需配置 API Key 即可完整体验。** 内置 Mock provider 产出流式回复、工具调用与 符合 schema 的结构化数据,编排链路(画布高亮、审批中断、沙箱执行)会完整走一遍。 接入真实模型请在「设置 → 模型接入」配置。启动时若环境变量中存在 `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` / `OPENAI_API_KEY`,将自动导入为 provider。 Mock 不会写引用标记:探索运行里报告撰写节点照样跑完,但里面的数字全标成「无证据」, 要看到逐段可点的出处得接真实模型。 内置 9 个模板,从「最小问答」到「可追溯周报」,既可直接运行, 也是各类节点用法的可执行文档。 --- ## 生产部署 **不用 Docker:** `./scripts/prod.sh` 构建前端并由后端一并托管页面,单进程、不热重载,启动于 http://127.0.0.1:8000 。前端产物不存在或比源码旧时才重新构建,`--build` / `--no-build` 强制构建或跳过。 `--port`(或 `AGENTLAB_PORT`)改端口;`--host` 监听所有网卡,同样**没有登录**,只在信得过的网络里开。 数据目录默认为仓库下的 `data/`,`AGENTLAB_DATA_DIR` 可改。只能单进程运行:运行管理器在进程内、数据库是 SQLite。 上传表格的版本存储同样只支持单进程运行(不要给 uvicorn 加 `--workers`):启动时会锁住数据目录,同一个数据目录 起了第二个进程时,后起的那个不做任何清理,上传、启用版本等写操作一律返回 503。 按期累积的数据源每个历史版本是一份完整副本,磁盘占用随期数和被引用的版本数增长。除当前版本和被运行引用的 版本外,每个数据源再保留最近启用过的 3 个版本,其余的会被回收、无法再启用。 **Docker 离线包:** `./scripts/package-docker.sh` 构建 Linux 镜像(默认取本机架构,目标机不同时用 `--platform linux/arm64` 或 `linux/amd64`),连同一键部署脚本、配置模板和部署说明打成 `release/agentlab-<版本>-linux-.tar`。拷到只装了 Docker Engine 的离线机器上,解包、按需修改 `agentlab.env`、运行 `./deploy.sh` 即可,不需要 docker compose。安装、升级、备份、沙箱权限与常见问题见 [deploy/README.md](deploy/README.md)。 镜像里默认带 microVM 的运行时和沙箱镜像(约 150 MB,`--no-microvm` 可不带)。目标机有 KVM 时,deploy.sh 先用临时容器试启动一台 microVM,起得来就把 `/dev/kvm` 交给容器,代码在 microVM 里执行,部署时不用联网。 没有 KVM(Mac、Windows 上的 Docker Desktop,多数云主机)时,默认权限下代码沙箱退回本地子进程,隔离边界只有容器本身。 --- ## 核心能力 | 领域 | 能力 | |---|---| | **可视化编排** | 18 种节点拖拽连线;运行时节点实时高亮、边上光点流动画、卡片内直接呈现流式 token;底部航迹把各节点的起止和并行度排在一条时间轴上,实时和回放共用;多 agent 节点在卡片里展开协作矩阵(谁在跑、几个人并行、省下多少) | | **对话式取数** | 「问数据」页:自然语言提问,系统自行接入数据源、生成工作流、执行并给出结论;答案里有数字时经报告撰写节点输出、逐段点得开;对话持久化、可分享链接 | | **多模型接入** | Anthropic / OpenAI / 任意 OpenAI 兼容服务(DeepSeek、Kimi、通义、智谱、硅基流动、Ollama 等)+ Mock | | **工具链** | 15 个内置工具 + 自定义工具(HTTP 模板 / 沙箱 Python)+ MCP server 接入;MCP 与自定义工具默认每次等审批,可逐个改成门控模型把关或始终允许 | | **Agent 护栏** | 同样的调用不重复执行、连续拿不到新信息就收尾、令牌与金额预算、上下文接近窗口先压缩再收尾;步数只留兜底(默认 100) | | **代码沙箱** | 双档隔离:系统沙箱(Seatbelt / bubblewrap,冷启动约 25ms)或 microVM(独立 Linux 内核,内存限额真实生效) | | **人工介入** | 执行暂停并落盘,人工批准 / 驳回 / 改稿后从断点继续;进程重启不丢状态 | | **断点续跑** | 失败的运行可从失败节点继续,已完成的节点不重复执行;允许修改节点配置,但拓扑必须一致 | | **知识库** | 向量 + BM25 混合检索、倒排索引、可选模型重排;PDF / Word / PowerPoint / HTML / Markdown 解析;向量模型可接本地端点 | | **表格入库** | Excel / CSV 上传后转为可用 SQL 查询的表,自动生成 `db_query__` 工具——数字由 SQL 计算得出,而非模型读取 | | **数据目录** | 给数据源的每张表记下粒度、业务主键、业务日期、有效记录条件,每个字段的含义、单位、度量类型和码值,以及表与表的关联;由外键、命名推断、数据剖析和模型起草,逐项确认后生效;助手建图、agent 查结构、SQL 检查都读它 | | **SQL 检查** | 按数据目录检查一对多关联后重复计算、存量跨时间相加、比率被相加等七类问题;助手自查时交回修正,受管模板发布时挡住,运行时标在查询结果上、契约指标降档 | | **跨库合并** | 合并查询节点:各库先聚合到同一个键,再在库外按键合并;截断的输入拒收,证据一路追到两边的原始查询 | | **长期记忆** | 跨运行的记忆读写与自动去重;Copilot 建图时主动记录应当记住的事实 | | **Skill 管理** | 将方法论抽象为可复用单元,挂载到节点上注入 system prompt | | **自然语言编排** | Copilot:一句话生成或改写整张图,自动排版;表多的库先按需求挑表、沿关联补上中间表,只给相关表的全部字段和数据目录;交付前按运行时同一套规则自查,查出会跑挂的问题先交回去改,改不好就不自动运行 | | **结构化成果** | JSON Schema 校验,不合格时将错误回传模型自动返工 | | **产出复核** | 运行结束后规则扫描事件流,发现异常才调用模型复核;复核可重组答案但**不得引入原答案中不存在的数字** | | **追踪与成本** | 全量事件落库可回放;逐节点耗时、token 用量、按目录价折算的成本 | | **三档出具** | 口径卡(受控指标集)+ 出具契约:叙述中每个数字回指指标集,或由报告撰写节点按引用逐段核对,formal / degraded / withheld 三档判定 | | **可点击证据** | 报告撰写节点只写引用标记,数字和表格由系统照快照填值;数字、表名字段名、知识库原话都点得开,一路指到查询结果里的那一格;结论句可要求挂依据或交给另一个模型裁判;证据清单可导出,报告导出为自包含 HTML(数字点得开口径与出处),另有封存哈希核对件「可验证报告」 | | **发布前检查** | 受管级别加证据门禁 G1–G5;问题给出一键修复 / 选项 / 交给 Copilot 三类修法,一律先预览、不许降低要求;老工作流一键升级为可追溯结构 | | **正式 / 探索分级** | 正式运行仅从已发布的不可变版本发起(按图哈希钉死);画布试跑自动标记为探索性 | --- ## 受限动态编排:模板定骨架,agent 填关节 大多数实际任务的步骤是已知的(写周报、核对数据、审批后发布)。让模型每次现场重新规划, 既是在重复发明已知答案,也使结果不可复现——而不可复现即不可验收。 AgentLab 的引擎是**编译式**的:运行时模型无法修改图结构,其自由被结构性地限制在 agent 节点内部与 branch 决策之中。在此基础上划分三层: ```mermaid flowchart LR T["模板层 · 骨架固定

已发布的不可变版本
WorkflowVersion + 图哈希
governed 级须过治理 lint"] J["关节层 · 有边界的自由

agent 工具白名单
branch 语义分类
缺数据时降档"] E["探索层 · 不进正式归档

画布试跑 / 临时图 / 追问
跑通的路径提取为草稿"] T ==>|约束| J E -.人工审核后发布.-> T ``` - **模板层** 正式运行只认已发布的不可变版本。发布后修改画布不影响已发布版本。 governed 级发布须通过治理 lint:禁用 supervisor(全动态归属探索层)、方法卡必须钉版本、 agent 不得关闭全部审批、output 必须配置出具契约,另有证据门禁 G1–G5 (见[受管门禁与发布前修复](#受管门禁与发布前修复))。 - **关节层** 授权范围内的自由。工具白名单衰减报表(`/api/governance/tool-usage`) 将「已授权但从未使用」的工具显性化,为收缩权限提供依据。 - **探索层** 结果自动标记为探索性,不进正式归档。跑通的路径经「提取模板」沉淀为草稿 (仅保留实际执行的节点,剪除未走的分支),人工审核后发布。探索问题聚类提示哪些追问 应当固化为模板中的一节。 ### 出具链路 模板⑧是最小示范:叙述由模型调用节点写,出口按数值回指。模板⑨把叙述换成报告撰写节点, 数字按引用填值(见[可点击证据](#可点击证据报告里的每个数都点得开出处))。两者的设计前提都是 **派生边界收死**:所有算术发生在口径卡节点内,叙述层只能引用。 ```mermaid flowchart LR A["取数
固定步骤"] --> B["口径卡
受控指标集"] B --> C["叙述
LLM"] C --> D["出具契约
数字回指校验"] D --> E1["formal"] D --> E2["degraded"] D --> E3["withheld"] A -.快照.-> S[("工件库
sha256 寻址")] B -.口径名 + 版本.-> S ``` ![出具链路模板](docs/images/graph-issuance.png) 校验器从叙述中抽取全部数字逐个回指指标集,支持千分位、百分数与比率互转、千/万/亿后缀、 按书写精度的舍入容差;日期、枚举序号、引用标记不参与回指。三档判定与缺数据声明随成果印出。 中文数字同样回指——否则换一种写法就绕过去了("增长三成、客户两千五百家"以前能盖 formal 章)。 成、倍、百分之、分之、一半按比例回指,"两千余"按区间 [2000, 3000] 回指;"一个问题""十分重要" "从三个方面看""三季度""几十家"这类虚指、序数、季度名、结构性列举不算结论数字。 实测记录:Opus 5 撰写的正文数字全部回指成功,但结尾「(约 100 字)」中的 `100` 被判定为 集合外数字并触发降档。处置方式是修改 prompt 而非扩大白名单——这正是校验器应有的行为。 **口径升版是被迫处置事件。** 钉住的方法卡出现新版本时,正式运行将被拒绝启动, 直至模板显式声明升版策略:`recompute`(回算历史)/ `dual`(双印)/ `incomparable` (标注不可比)。此处无默认值——口径变更最容易静默翻车的形态是「数字看起来都对, 只是与上周不可比」。 每次运行的完整证据存于内容寻址的工件库(`data/artifacts/`,sha256 即地址,取回时复验哈希); 运行终态的事件清单哈希记录在 Run 上(封存到 `run.finished` 为止),事后修改流水将无法对齐。 运行详情页一键核对,接口是 `GET /api/runs/{id}/verify`;封存之后追加的事件不在核对范围内。 --- ## 可点击证据:报告里的每个数都点得开出处 按数值回指只能在出口回头猜出处,同值的指标一多就猜错。可点击证据把核对挪到写的那一刻, 出处直接写进报告的数据结构里。 ![证据面板](docs/images/evidence-panel.png) 记录页的「证据」页签:点开「环比 8.7%」,右栏从口径卡的原式、代入式一路指到两次查询里被引用的那一格, 末尾是封存核对;左下是证据清单。Mock 写不出带引用的报告,这次运行用前端测试夹具 (`frontend/src/run/__tests__/evidence-query.json`,示例数据)伪造,列表里其余几条是 Mock 真跑的。 **引用即渲染。** 报告撰写节点(`report`)里模型只写引用标记,数值由系统照快照填进去: ``` [[m:gmv]] [[m:gmv|万]] 口径卡指标;换算显示也由系统完成 [[i:week]] 运行输入 [[v:Q1.r0.gmv]] 查询快照里的一格,取回时复验哈希 [[table:Q1 cols=region,amount rows=0-4]] 系统照快照生成整张表,每一格都点得开 [[t:orders]] [[c:orders.week]] 表名、字段名;写在反引号里的名字同样核对 [[q:K1|原话]] 知识库原话,须在那次检索命中的片段里逐字出现 [[see:m:gmv,Q1]] 结论句的依据,挂在句末,不渲染 ``` 流式输出里看到的就是最终数字。裸数字、解析不了的引用当场判违规、交回去重写(`max_repairs`,默认 1 次), 仍不过关时按 `on_violation` 让节点失败(正式运行默认)或把违规标在报告里(探索运行默认)。 出具契约写了 `report_from` 就按引用核对:取回报告文档时复验哈希、独立重算一遍,全部带引用才判完整出具。 **取数链路。** agent、工具节点、代码节点、知识检索取数时都记进证据台账,报告目录按台账编出 Q1… / K1…。 口径卡表达式可以写 `cell(nodes.pull, 0, 'gmv')`,出处精确到单元格。agent 开 `cite_fields`(配合 `output_schema`) 后,每个字段标出取自哪次查询的哪一格,再到快照里核对:对不上以快照为准并告警,查不到的记为空值,不兜底成 0。 代码节点分 `evidence_role`:`source` 是取数,`compute`(默认)是计算——沙箱算出来的数不能被报告直接引用, 要进口径卡。数据源可设 `mask_columns` 遮罩列(只减少暴露,不是安全边界)。 **点开看到什么。** 数字:指标的原式、代入式和复算结果,输入与快照是否一致,查询的 SQL、被引用的行加前后 各 2 行、高亮到格,最后是封存状态;只认封存范围内的事件交回过的工件。上传表格的单元格可以追到原表格子 (推断的来源)。表名、字段名:出现在哪次查询、类型、 所属的表。表结构快照、SQL、结果列里都没有的名字标成可疑实体(「可能是编造的名字」,附最接近的 3 个); 快照被截断、核对不了的只说核对不了,不冤枉成编造。 **结论句。** 报告节点的 `claims`:`off`(默认)不管;`require_citation` 要求结论句挂依据,没挂的计入缺口; `judge` 再请另一个模型按证据逐句判为支持 / 部分支持 / 不支持 / 不是结论,只标注、不改写正文,裁判模型和 写作模型相同时给警告。正式运行在节点内判,判定随报告一起封存,不支持的按 `on_unsupported` 降档或不予出具; 探索运行点开哪句判哪句,判定作为封存之后追加的事件记下,封存核对照样一致。每份报告的句数、金额、时长, 每次点击的金额,每天的总金额都有上限(默认 40 句、$0.05、30 秒,每次点击 $0.01,每天 $2),每一项都能设成不限。 **审计与旧运行。** `GET /api/runs/{id}/evidence/audit` 按无证据 / 可疑实体 / 有出处 / 旧运行猜测分组, 可导出 JSON 和 CSV。没有契约的旧答案按数值和已封存的查询单元格、口径卡值比对,每个数最多 3 个候选, 默认折叠,写明「猜测的来源,不能当证据」。老工作流可以一键升级为可追溯结构:模型调用写的报告换成报告撰写节点, 问数据的 input → agent → output 中间插一个报告撰写,配了 `output_schema` 的 agent 打开 `cite_fields`; 沙箱代码喂口径卡的只建议标成 `source`,不替人改。升级只给预览,人确认后才保存。 **导出两份。** 问数据答案和运行详情成果上的「导出」把报告导出成一个自包含的 HTML 文件 (`GET /api/runs/{id}/report.html`):正文由服务端渲染,不执行脚本也读得到(邮件附件预览常常不执行脚本); 执行脚本后每个数字、无证据的数字、可疑的表名字段名都点得开——面板是网页上证据面板同一个拼法的 出处链(口径、来源、计算方式、SQL、被引用的行),封存状态逐项标明;遮罩列照网页上的规矩写「已遮罩」。 没封存的运行照常导出(和网页上读报告一个道理)。记录页「凭证详情」里的「导出可验证报告」 (`GET /api/runs/{id}/bundle.html`)是另一份:封存核对件,收件人不连服务、在浏览器里重算哈希,核对每个 数字、文档和运行记录没被改过;数字的出处面板同样带口径、原式、代入式、输入和复算,口径卡输入指向的 查询快照也随文件附带、引用链认得这条引用。要证明没被改过用可验证报告,要让人读报告、查数字的来历用报告导出。 ### 受管门禁与发布前修复 受管级别在治理 lint 之外加了证据门禁,只看图和配置,让「每个数都点得开」在结构上成立: - **G1** 带契约的出口,成果字段的文字只能来自报告撰写节点 - **G2** 模型写的文字不绕过报告撰写节点直接流进出口 - **G3** 报告撰写节点写明 `numbers: strict`、`on_violation: fail`、`claims: require_citation`(或 `judge` 并写预算) - **G4** 口径卡的输入不来自计算角色的沙箱代码 - **G5** 给口径卡供数的 agent 配 `output_schema` 并开 `cite_fields` 受管级别下是错误、挡住发布;已发布级别只给警告;已经发布的版本照常运行,下次发布时按新规则检查。 发布弹窗打开就先跑一遍发布前检查(`POST /api/workflows/{id}/publish-check`,和真正发布同一套口径), 问题面板里平时也看得到。修复(`/autofix`)分三类:答案唯一的一键修复;要人拿主意的给选项、不替人选; 结构性的交给 Copilot。一律先预览,不自动保存、不自动发布。删节点、删契约、放宽审批、关掉 strict、改级别 这类降低要求的改动一律拒绝,Copilot 的方案也要过这一关;应用后重跑检查,错误必须变少、不能冒出新的。 一键升级(`POST /api/copilot/upgrade-evidence`)过的也是这道检查。 --- ## 数据目录:让助手和 agent 看懂业务数据 表名和字段名说不清业务:一行代表一张门票还是一次检票,`visitor_count` 能不能按天相加,两张表按哪一列连、 连上之后会不会一对多。助手(Copilot)建图、agent 写 SQL 时只能猜,猜错了查询照样能跑,数却是错的。 数据目录把这些事实记下来,交给三处读: - **助手建图**:表多的库先按需求挑表,再沿关联关系补上连接路径上的中间表,只给这些表的全部字段、目录和 可以照抄的连接条件,其余表只列名字。挑不出时退回只列表名,不阻断生成;只探查了一部分表时如实说明。 - **运行时查结构**:`db_schema` 返回表结构时附上目录,问数据页里现场写 SQL 的 agent 也用得上。 - **SQL 检查**:按目录检查七类问题——一对多关联后重复计算、存量跨时间相加、比率被相加或平均、漏了有效记录条件、 码值不存在、没用约定的业务日期、关联条件对不上已知关系。助手自查时交回修正,受管模板发布时挡住,运行时标在 查询快照上,契约指标的来源查询有错误时出具降档。解析不了的写法一律跳过:宁可漏报,不误报。 目录里每一项都带来源和状态。来源是数据库注释、外键、命名推断、数据剖析、模型起草或人工;状态是推断、已验证、 已确认或已驳回。推断的项只作提示,已验证、已确认的才进入错误级检查;驳回的项保留编号,不会被下一次起草或 推断带回来。 | 环节 | 做法 | |---|---| | 起草 | 探查时读外键和唯一约束;按 `xxx_id`、`xxxId` 推断关联,表名带业务前缀的也认;可选让模型起草中文名、粒度、表类型、业务主键、业务日期和每列的含义、单位、度量类型。导入表格的源已有系统按核对结果生成的说明,目录不重复、不改写它 | | 模型起草 | 作为后台任务逐表进行,每张表记下状态和原因,写完一张就并入目录,宽表每写完一段就并入;关掉对话框不停,可以停止、只重试失败的表,清单上标出上次起草失败或只完成一部分的表。停止、服务重启时已经写进目录的段保留;服务重启时没跑完的任务标成中断,不会自己续跑,点重试重新起草这些表(已写进的不会丢)。不设总时长上限,靠止损和手动停止:最近 10 次调用有 7 次没写出来时后面的段不再拆小,同一张表接连 3 段都没写出来就跳过它剩下的段。保留模型的思考,输出额度 32768(服务方不收时退回 8192);每次调用最多 40 列,宽表先起草表级的项、再把列均分成几段起草,按表的先后排队,不插到窄表前面;额度用完或超时就拆小再问(超时的两半同时问,拆到每段 10 列仍接连超时就不再拆),累计两次后新开始的段减半(最低 10 列),每段连同它分开、拆小的各块最多调用 8 次,进度上看得到段被分成了几块,缺的表和列只补问缺的,被截断的回复先留下已经写完整的部分;最近 6 次调用有 4 次没写出来时提醒换模型。没写出来的原因写清是第几段、哪几列,「已拆小重试」只跟在真拆过的段后面。同步起草和后台任务互斥。重新起草时模型这次留空或没通过校验的项不删以前起草的;模型拿不准、留空的列(目录里还没有中文名和含义的)逐表记下,可以补充起草要求后重起草。库注释就是表名、列名本身或者是标识符写法的,不记进目录(不当中文名,也不当说明、含义)。数据源上存的「起草要求」和这次的补充说明一起交给模型,帮它理解业务、统一用词,不能推翻「只记数据事实」的规矩,交回的内容照样逐项校验。用哪个模型在「设置 → 偏好设置」的「起草模型」里选,空着跟随助手的模型;按配方导入表格的 AI 起草也用它 | | 防错事实的提议 | 模型起草时另提议「有效记录条件」(一段 SQL 条件)和「去重规则」,各带依据;开了剖析的源把目录里的码值一起交给模型(遮罩列不给)。条件按数据源方言用 sqlglot 解析,只能引用这张表的列,不能有子查询、聚合、窗口函数,只允许几个常用函数,不过就不收(逐表说明里写原因);开了剖析的源上经只读守卫数出排除比例,排除 0% 或 100% 的不收,收下的在说明里写「剖析核对:排除 x% 的行」。这些都记为推断,SQL 检查的错误级规则只认人确认过的,提议在审阅队列里排在前面 | | 审阅队列 | 只列推断的项,按「表被运行用到的次数 × 这一项多影响数字 × 来源」排序:有效记录条件、去重规则、业务日期、关联关系最前,业务主键、度量类型次之,中文名、含义、单位靠后,每项写明排在这里的理由;按类别和表筛选,多选批量确认、驳回或撤销审阅(逐表乐观锁,冲突逐条标出);看得到一张表被哪些运行用过,以及常用表、全部和影响数字的项各确认了多少 | | 表助手 | 表详情页里和助手对话完善这一张表:它看得到表结构、目录现状(每项的来源和状态)、关联关系、剖析结果、起草要求和上次起草留空的列(遮罩列只给列名),按你的指示成批提出修改,每项带改前、改后和依据(照目录的校验:公式不收、列名要在表结构里),拿不准的会反过来问你;只在开了剖析的源上经只读守卫跑查询核实(每次请求最多 5 条,结果先遮)。提案攒在「待保存的修改」里,确认后一次保存(一次最多 200 项),记为人工确认,每项的依据另存为这一项「确认时的依据」(和备注分开,表详情里看得到;撤销审阅回到推断时去掉)。对话不存服务端,只改目录、不碰业务库 | | 导出与导入 | 导出成一个 JSON 文件(全部表的目录、各项状态和起草要求),在另一个实例或数据源上导入:先预览哪些表、列对得上,每一项会新增、更新还是保留,冲突和跳过的逐条列出;人确认、驳回过的项照带,目标里已经确认过而值不同的保留目标、记成冲突,文件里推断的项只填空缺,表结构里没有的列跳过并写原因;有起草在进行时不能导入。起草要求只在目标没有时写入 | | 数据剖析 | 默认关闭,按数据源开启。只读、经过守卫,单条查询和整次剖析都有上限,大表用统计信息估算行数,遮罩的列不取样。抽样核实推断出的关联,补上覆盖率和基数,站得住的升为已验证;状态列给出码值候选,含义等人填写 | | 对话里纠正 | 用户说「status=9 表示作废」,助手给出一条目录修改提案,列出改前、改后和理由,人确认后才保存;公式不收,留给口径卡 | | 版本与提醒 | 目录随表结构快照冻结进证据,每次运行用的是哪一版查得到。发布时记下模板引用的表的目录版本,正式运行时目录有变化就提醒;agent 运行时才写 SQL 的,按「可能涉及」单独提醒。只提醒,不拦运行 | 目录只记数据事实,不记计算公式:「在园人数是存量,不能跨天相加」进目录,「环比 =(本月 − 上月)÷ 上月」进口径卡。 这也是不另做一层指标编译式语义层的原因——口径已经由口径卡和版本固定在管,再造一层就会出现两份口径。 跨库的问题不做联邦查询:各库先聚合到同一个键,再用**合并查询**节点在库外按键合并(内存 SQLite,同一套只读守卫)。 被截断的输入拒收,键类型不一致、合并后行数变多都会提醒,报告里的数能一路追到两边的原始查询。 --- ## 工作流形态 **人工介入与分支**——执行在 `human` 节点暂停并落盘,人工决策后沿对应出口继续: ![人工审批流程](docs/images/graph-human.png) **多 Agent 协作**——调度者按进展将任务分派给多个专家,属探索层能力,governed 级发布禁用: ![多 Agent 协作](docs/images/graph-supervisor.png) 节点间通过模板语法传递数据: ``` {{ input.question }} 入口输入 {{ vars.answer }} 某节点 assign_to 写入的变量 {{ nodes.n1.text }} 指定节点的输出 {{ vars.items | json }} 过滤器:json / compact / upper / length / first … ``` **条件和表达式不是模板。** 分支条件、while 循环条件、整形表达式、口径卡指标、跳过条件 这几处是直接求值的表达式,变量直接写:`vars.gate != 'ok'`、`len(vars.items) > 0`。 多套了 `{{ }}` 意思没有歧义,照 `vars.gate` 理解、只给一条提示;真写错的在校验阶段就拦下, 不会等跑到那一步才炸——`| length` 这类过滤器(改写 `len(x)`)、`{'a', 'b'}` 集合(改用列表)、 不认识的函数、把 `==` 写成 `=`、引号里的 `{{ }}`(它不会被渲染,条件永远不成立)。 校验、Copilot 自查和运行时用的是同一个解析器,不会出现"校验说能跑、跑起来报错"。 **代码节点的结果要 print 出来。** `assign_to` 拿到的是 stdout:脚本最后一行写个裸表达式 不会输出(那是 notebook 的行为),变量会是空的。要交给下游一个对象就 `print(json.dumps(结果))`,stdout 是 JSON 时会自动解析成对象。设了 `assign_to` 却找不到 任何输出的 Python 代码,校验会直接拦下。 连线即并行:单个节点连出多条边构成 fan-out,多条边汇入同一节点时自动等待汇聚—— 汇合节点等图里其余待执行的任务都跑完再执行一次,支路一长一短也不会被触发两遍; 分支之后的二选一汇合不会去等没走的那条。代价是它也会等图里与它无关、恰好还在跑的支路。 自动排版按拓扑分层:并行的支路落在同一列,层内按中位数启发式排序以减少交叉, 分支节点的出口按 yes/no 的声明顺序自上而下,循环的回边不参与分层(否则循环体会 被压到循环节点左边)。层与层之间按"穿过这条走廊的连线在竖直方向最多重叠几条" 预留走廊宽度,前端 `canvas/routing.ts` 据此给每条线分配独立的竖直车道——扇出、 汇聚的线因此不会叠在同一段上,跨层的长边则从没有节点的空档绕过去。 --- ## 节点类型 | 分类 | 节点 | 说明 | |---|---|---| | 起止 | `input` `output` | 声明输入字段 / 收集结构化成果 | | 模型 | `llm` `agent` `supervisor` `report` | 单次调用 / 带工具循环 / 多 agent 协作 / 报告撰写(只写引用标记,数字由系统填) | | 执行 | `tool` `code` `merge` | 直接调用工具 / 沙箱内执行代码 / 合并几次查询的结果(跨库按键对齐) | | 控制 | `branch` `loop` `subgraph` | 条件或语义分支 / 遍历与条件循环 / 嵌套工作流 | | 上下文 | `memory` `retrieve` `transform` | 长期记忆读写 / 知识检索 / 数据整形 | | 把关 | `human` `validate` `metrics` | 人工介入 / Schema 校验与自动返工 / 口径卡 | --- ## 架构 ```mermaid flowchart TB subgraph FE["前端 · React 19 + React Flow + Zustand"] F1["画布编辑"] F2["问数据"] F3["运行追踪 · 证据面板"] F4["decode.ts
事件 → 人类可读的唯一翻译层"] end subgraph BE["后端 · FastAPI + LangGraph"] B1["engine
编译 · 调度 · 中断恢复 · 出具与证据"] B2["providers
多模型接入与成本估算"] B3["tools
注册表 · MCP · 数据源"] B4["memory
记忆 · 知识库 · 混合检索"] B5["sandbox
microVM / Seatbelt / bubblewrap"] end subgraph ST["存储"] S1[("agentlab.db
业务数据")] S2[("checkpoints.db
执行断点")] S3[("artifacts/
内容寻址工件库")] end FE <-->|REST + WebSocket| BE B1 --> S1 & S2 & S3 ``` ``` backend/ FastAPI + LangGraph app/engine/ 画布 JSON → StateGraph 的编译与执行 schema.py 图定义与静态校验 compiler.py 编译:节点包装、事件、重试、条件路由 runner.py 执行调度、事件总线、中断与恢复 issuance.py 出具契约:数字回指与三档判定 evidence.py 可点击证据:证据台账、引用标记、报告文档的切块与核对(纯函数) judge.py 结论句裁判:预筛、成本上限、判定写回 governance.py 发布门禁:受管 lint 与证据门禁 G1–G5 autofix.py 发布前自动修复;upgrade.py 一键升级为可追溯结构 guards.py agent 护栏:重复调用、预算、上下文压缩 review.py 产出复核:事件流规则扫描 + 模型复核 expressions.py 模板插值 + AST 白名单表达式求值 nodes/ 各类节点的执行器(report.py 是报告撰写) app/data/ 数据源接入:连接、SQL 守卫、结构探查、表格导入 app/providers/ 多模型接入与成本估算 app/sandbox/ microVM / Seatbelt / bubblewrap / 裸子进程四后端 app/tools/ 工具注册表、内置工具、MCP、自定义工具 app/memory/ 记忆、知识库、倒排索引、混合检索、文档解析 app/api/ REST + WebSocket(证据接口在 evidence.py) frontend/ React 19 + React Flow + Zustand src/canvas/nodeDefs.ts 节点元数据——属性面板、节点库、连接桩均由其驱动 src/canvas/routing.ts 连线走线:端口错开、走廊车道、长边绕行 src/run/decode.ts 事件流 → 人类可读步骤的唯一翻译层 src/run/EvidenceDoc.tsx 报告逐段渲染:数字、名字、原话都是可点的片段;EvidencePanel.tsx 是出处链 src/store/studio.ts 画布状态 + 事件流到高亮的映射 environment.yml conda 环境定义 scripts/dev.sh 一键启动 scripts/check-all.mjs 前端检查的总入口:分道并行跑全部 check-*.mjs,汇总成一张表 ``` --- ## 关键设计决策 ### 选用 LangGraph 可视化编排的难点不在绘制节点与连线,而在暂停、恢复与回放。LangGraph 的 checkpointer 与 `interrupt()` 将这几件事做在了底层:人工介入是一次中断加一次 `Command(resume=...)`, 进程重启后仍可从断点继续;`aget_state_history()` 直接给出每一步的状态快照。 由此派生的第二项收益:**agent 内部的工具审批不重复计费**。`interrupt()` 会使整个节点重放, 朴素实现将重复执行此前的模型调用。agent 节点内的模型调用与工具执行均包裹在 LangGraph 的 `@task` 中,结果进入 checkpoint,重放时直接读取缓存。 ### 事件流是唯一的可视化数据源 节点通过 `get_stream_writer()` 发出事件,runner 转换为 `RunEvent` 落库并广播。 画布高亮、token 流、工具卡片、时间线全部由同一份事件驱动——刷新页面或中途接入 均可获得完整过程(WebSocket 先补历史再接实时)。 前端侧对应地只有一个翻译层 `run/decode.ts`:画布右栏、问数据页、运行详情页共用它。 分设多套的后果是同一次运行在三个页面讲出三个版本,而使用者无从判断哪个为真。 ### 画布上的动效是"正在发生的事",不是装饰 画布上每一个会动的东西都对应一件真实事件:边框上转一圈光弧 = 这个节点正在跑, 扫描线扫过 = 正在产出,绿色扫一遍 = 刚跑完,抖动 = 刚失败,琥珀呼吸 = 在等人, 边上的光点 = 数据正从这边流向那边。跑一张十几个节点的图,一眼要能看出"谁在动"—— 纯装饰的动效做不到这件事,只会让画布变吵。 多 agent 节点会**在卡片里展开协作矩阵**:花名册固定不重排,被派出去的人亮起来并 显示当前任务,条形长度按"这一轮里最慢的那个人"归一化——三个人并排三条长短不一的 条,墙钟为什么等于最长那条而不是三条之和,不用解释就看懂了;页脚给出并行一共省下 多少。分支节点跑完会点亮命中的那个出口、压暗其余,实际走过的边留在画布上,就是 这一次的执行路径。 矩阵和右栏泳道读的是**同一份** `reduceTeam`(`run/decode.ts`),两处不会给出不同的 并行度。系统里关掉动效时(`prefers-reduced-motion`)光效整体不绘制而不是停住—— 停在最后一帧的光弧和堆在起点的光点看着像渲染坏了;静态下"谁在跑"由实心圆点和 "进行中"三个字承担。 ### agent 的护栏取代固定步数 以前 agent 节点一律「最多 12 步」。固定步数想挡的是四件事,每件都有更贴切的挡法(`engine/guards.py`): - **循环不收敛**:同一个工具、同样的参数第二次不再执行,把上次的结果交还给它;连续 3 步拿不到新信息就收尾 - **花费越滚越大**:每个节点记令牌预算(默认 200 万)和金额预算(默认不限),都可以在设置里改成不限 - **上下文会满**:用到窗口的 60% 先压缩早期的工具结果,到 85% 就收尾 - **真正失控**:步数只留兜底,默认 100,设置里可改,`AGENTLAB_MAX_AGENT_STEPS` 封顶 临近任何一个上限先提醒模型一次。上限在运行发起时快照,恢复和接着跑沿用同一份——审批恢复时节点整个重放, 上限一变,循环停的位置就和 checkpoint 对不上。下游结构化校验失败时,报错写明是上游哪个 agent 提前收了尾。 ### 沙箱:双档隔离,以及必须声明的缺口 轻量档使用操作系统自带的访问控制:macOS 的 Seatbelt(`sandbox-exec`)、Linux 的 bubblewrap, 冷启动约 25ms,无需额外安装。重量档为 microVM(libkrun + Hypervisor.framework), 启动带独立内核的虚拟机。 两档共同保证三项约束:**默认断网**、**家目录不可读**、**仅工作区可写**。Seatbelt 侧策略 由内核强制且对子进程继承——沙箱内 `subprocess` 启动的 `cat` 读取家目录同样返回 `Operation not permitted`;解释器刻意使用 `sys.base_prefix` 下的干净 Python 而非项目环境, 因此沙箱代码无法 import FastAPI、SQLAlchemy 及任何凭据处理相关模块。 **Seatbelt 的局限**:它提供访问控制而非虚拟化。进程表共享(可见宿主机进程列表); 代码以当前用户身份运行(不似容器会降权至 nobody);**macOS 不强制 `RLIMIT_AS`, 内存用量无法限制**(实测设定 256MB 仍可分配 900MB)。CPU 时间、单文件大小、 墙钟超时三项有效。 **microVM 补齐的正是内存这一项。** 本机实测:宿主为 Darwin 27.0.0,VM 内为 Linux 6.12.99 (libkrunfw 编译),PID 1 为 `init.krun`,`/Users` 在 VM 内不存在,`free` 仅显示 512MB。 申请 900MB 将被内核 OOM killer 终止而 VM 存活——`RLIMIT_AS` 在 macOS 上形同虚设的问题 在此不是被修复,而是结构上不存在。两台 VM 之间亦互不可见。 代价:运行时约 50MB,OCI 镜像**首次拉取实测 54.5 秒**;缓存后热启动 0.19 秒、 VM 内执行 7–30ms。成本集中在首次而非每次——因此 auto 模式在镜像未缓存时优先使用 Seatbelt, 避免一分钟的等待(显式指定 strict 不受此限)。 > **网络隔离不完整,此项须置于前列。** > `network=false` 时 HTTP/HTTPS 与域名解析均被切断,但 **UDP/53 无法拦截**——实测手写 > DNS 包仍可获得真实响应。`default_egress=DENY`、`Rule.deny_dns()`、显式 deny UDP、 > `max_connections=0` 均已尝试,UDP 通道始终无法封堵(`deny_dns` 生成的规则仅作用于宿主, > DNS 出口在 microsandbox 的网络栈中硬编码放行)。 > 即 **DNS 隧道外泄通道始终开放**。该隔离足以防范「代码意外联网安装依赖」, > 不足以防范「恶意代码窃取数据」。设置页据此将网络一项标注为「部分生效」的黄色而非绿色—— > 拦截了一半却显示为全绿,比显示为全红更危险。真正的外泄防护须在网络层实施。 ### 检索为何是混合的 默认 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 个百分点,结论的方向可信而精度不可过度解读。 向量模型可接任意 OpenAI 兼容端点(LM Studio、Ollama、vLLM、TEI)。维度通过实际探测获得 而非按模型名推断——本地部署的模型维度各异(bge-m3 为 1024、Qwen3-Embedding-4B 为 2560), 推断错误的后果不是偏差,而是全部存量向量被判定为与当前 embedder 不匹配,检索整体退回关键词。 ### 表格数据必须成为表 Excel / CSV 若传入知识库,只能被切块检索,数字即成为模型从片段中「读取」的结果。 而本项目的基础约定是「所有算术下沉到 SQL 或口径卡」——产出复核层能拦截凭空出现的数字, 无法拦截「从一堆片段中读错了一格」。 因此表格上传后落为 SQLite 文件并注册为 `kind=sqlite` 的数据源,自动生成 `db_query__` 工具。列类型逐列推断(INTEGER / REAL / TEXT),空单元格存为 NULL—— 整数列若落为 TEXT,`ORDER BY` 将退化为字典序(`'10' < '9'`),且不产生任何报错。 ### 安全边界 - 分支条件使用 AST 白名单求值器而非 `eval` - HTTP 工具解析域名后逐 IP 检查,拦截内网、回环与云元数据地址,且不跟随重定向(逐跳重新校验) 本机代理开 fake-ip 时,解析结果是 198.18 段的假地址:配置 fake-ip 网段后,这类结果改向公共 DNS(DoH) 核实真实地址再判断,核实不了按拦截处理 - 文件工具路径 `resolve()` 后必须仍位于工作区内 - SQL 守卫采用白名单动词判定 + 正文扫描 + 多语句拦截 + 结果集上限;`DROP` / `TRUNCATE` / `GRANT` 等 在可写数据源上亦一律拒绝。以查询开头却会写的语句(`WITH … DELETE`、`SELECT … INTO`、 `EXPLAIN ANALYZE`、`PRAGMA … =`、`set_config()` 等)按写操作处理 - 只读数据源在**连接层**也只读,不只靠守卫猜:SQLite 以 `mode=ro` 打开,PostgreSQL / MySQL 会话事务只读。**Oracle 没有会话级只读开关**,只剩守卫一道——只读数据源请配只读账号 - 人工审批只认明确的批准(`true` / 同意 / approve),看不懂的回复一律按拒绝处理。 危不危险逐次判定:同一个 `db_query__<源>`,查询直接执行,可写源上的写操作要先确认, 确认后才提交。协作团队的成员停不下来等人,需要确认的调用不执行,会告诉成员换一种做法 - MCP 与自定义工具平台看不出它做什么,默认每次调用都等审批(三档:`ask` / `gated` / `always`)。工具页可逐个改成「始终允许 · 门控把关」 (每次调用先由门控模型审一遍参数,拿不准、出错、超时都交还给人)或「始终允许」;审批卡上的 「始终允许」等于前者。档位在发起运行时快照,恢复和接着跑沿用同一份;正式运行不看档位,一律等审批 - API Key 经 Fernet 加密落库,仅向前端返回掩码 --- ## 运行环境 ### 沙箱后端选择 设置页「运行环境」列出全部候选及其可用性,并标注当前选中项。安装 microVM 依赖、镜像已缓存, Linux 上还要拿得到 `/dev/kvm` 时,auto 选择 `microvm`;否则 macOS 使用 `seatbelt`、Linux 上安装 `bwrap` (`apt install bubblewrap`)后使用 `bubblewrap`。装了 `bwrap` 还要能真正创建命名空间才算可用: 启动后首次选择时实际起一次 `bwrap` 探测,Docker 默认权限、Ubuntu 24.04 的 AppArmor 限制下会失败并跳过, 「选择原因」写明。均不可用时降级至 `local`——**该路径不含任何访问控制**,界面明确标红。可通过 `AGENTLAB_SANDBOX_BACKEND` 强制指定, 亦可在代码节点上单独选择隔离档位(strict = microVM,fast = 系统沙箱)。 ### 启用 microVM 三项准备,**均为一次性**——日常启动仍只需 `./scripts/dev.sh`: 1. Python 包:使用 `environment.yml` 创建环境时已包含(含 `[microvm]` extra) 2. 运行时(约 50MB,落于 `~/.microsandbox`):每台机器一次 3. OCI 镜像(实测约 55 秒):每个镜像一次 第 2、3 项手动执行,装完用 `doctor` 检查本机的虚拟化条件,再重启 `dev.sh`: ```bash conda run -n agentlab python -c "exec('import asyncio, microsandbox as m\nasync def main():\n await m.install()\nasyncio.run(main())')" conda run -n agentlab microsandbox pull python:3.12-slim # 默认镜像,换了 AGENTLAB_MICROVM_IMAGE 就拉那个 conda run -n agentlab microsandbox doctor ``` 镜像要先拉:auto 只选镜像已经缓存的 microVM(免得第一次跑代码卡一分钟),没缓存就跳过它,而镜像又只在 microVM 里跑代码时才会拉——不手动拉,auto 永远选不到它。后端选定沙箱后整个进程都沿用,装好之后要重启 `dev.sh` 才会重新选。不想等也可以显式指定 `AGENTLAB_SANDBOX_BACKEND=microvm`,第一次跑代码时再拉镜像。 拉镜像报 `registry error: error sending request for url (https://index.docker.io/...)` 是连不上 Docker Hub。 msb 认 `HTTPS_PROXY` / `ALL_PROXY`,给它指一个代理:`HTTPS_PROXY=http://<代理地址>:<端口> microsandbox pull python:3.12-slim`; 或者从能连上的镜像源拉(写完整名字,如 `<镜像源>/library/python:3.12-slim`),再把 `AGENTLAB_MICROVM_IMAGE` 设成同一个名字,让 AgentLab 用的正是拉下来的那一份。 `install` 要在正在跑的事件循环里调用:写成 `asyncio.run(m.install())` 会先在循环外调用它,报 `RuntimeError: no running event loop`,所以包了一层 `async def`。Linux 要有 `/dev/kvm`,当前用户还得有权限—— doctor 报「/dev/kvm is not accessible by your user」时: ```bash sudo usermod -aG kvm $USER # 永久生效,重新登录后才算数 sudo setfacl -m u:$USER:rw /dev/kvm # 当前会话立即可用 ``` 也可以直接 `microsandbox doctor --fix`。改完再跑一次 doctor,应当全是 ✓。doctor 全绿不等于第 2 项装好了: 它只查 wheel 里自带的 msb 和 libkrunfw,AgentLab 用 `microsandbox.is_installed()` 判断,看的是 `~/.microsandbox`, 所以上面那条安装命令照样要跑。 三项均落于磁盘,重启机器或重开终端无需重来。设置页「运行环境」中 `microvm` 条目的 `warm` 为 true 即表示三项齐备,auto 将自动升级至 `microvm`。镜像可通过 `AGENTLAB_MICROVM_IMAGE` 更换,闲置 VM 回收时间由 `AGENTLAB_MICROVM_IDLE_SECONDS` 控制。 ### 可选依赖 `environment.yml` 默认已经全部装上。单独补装时带上锁文件,版本才和测过的一致: ```bash pip install -e 'backend[docs]' -c backend/requirements.lock # PDF / Word / PowerPoint / Excel 解析 pip install -e 'backend[db]' -c backend/requirements.lock # MySQL / PostgreSQL / Oracle 驱动 pip install -e 'backend[microvm]' -c backend/requirements.lock # microVM 沙箱 pip install -e 'backend[dev]' -c backend/requirements.lock # pytest ``` ### 依赖与锁文件 `backend/pyproject.toml` 写版本范围,`backend/requirements.lock` 写精确版本:后端全部 依赖(含间接依赖、全部 extras)锁在测试跑过的那一套上,跨平台通用。`environment.yml` 装包时以它为约束,所以换一台机器装到的也是同一套。 LangGraph / LangChain 一族在 `pyproject.toml` 里只放行到下一个小版本:引擎直接依赖 它们的断点格式、interrupt / resume 和流式事件,这些在小版本里也改过行为。aiosqlite 与 SQLAlchemy 同理——`db/base.py` 给 aiosqlite 打了补丁,连接池断开连接时会走到它。 升级依赖要主动做,改完跑一遍测试再提交: ```bash ./scripts/lock-deps.sh --upgrade-package langgraph # 升一个包(连带它必须跟着动的) ./scripts/lock-deps.sh --upgrade # 全部升到 pyproject 允许的最新 ./scripts/lock-deps.sh # 只加了新依赖:补上它,已锁的不动 pip install -e 'backend[microvm,dev,db,docs]' -c backend/requirements.lock # 装到当前环境 ``` 重新生成锁文件需要 [uv](https://docs.astral.sh/uv/);装依赖本身只用 pip。 ### 数据位置 全部位于 `data/`:`agentlab.db` 业务数据、`checkpoints.db` 执行断点、 `artifacts/` 工件库、`workspace/` 沙箱文件、`uploads/` 上传文件、`.secret_key` 加密密钥。 删除 `data/` 即恢复出厂设置。 --- ## 常见问题 **模型返回空内容。** Claude 4.6 之后的模型默认启用 thinking,`max_tokens` 过小会出现 「思考耗尽额度,正文为空」。将节点的 max_tokens 调大,或将思考模式设为关闭。 时间线中会给出相应提示。 **中转网关返回 401。** 部分网关仅接受 `Authorization: Bearer`,且额外携带 `x-api-key` 即会拒绝。在 provider 设置中勾选「用 Authorization: Bearer 认证」。 **数据源结构探查失败。** 探查失败与「尚未探查」是两种状态:前者点击按钮无法解决, 需先处理超时或更换 schema。数据源卡片会显示失败原因与该服务器上的可选库列表。 agent 侧同样会收到实情,并被告知可直接查询 `information_schema` 自行确认表结构。 **网页工具报「解析到内网地址 198.18.x.x,已拦截」。** 本机代理(Clash / mihomo 等)开了 TUN + fake-ip, 查任何域名都先拿到 198.18 段的假地址,被当成内网拦掉了。在 `.env` 里加一行 `AGENTLAB_HTTP_TOOL_FAKE_IP_RANGES=198.18.0.0/15`,重启后端即可:落在这个段里的解析结果会改向公共 DNS 核实真实地址,公网放行、内网照拦。核实用的 DoH 地址由 `AGENTLAB_HTTP_TOOL_DOH_URLS` 指定(逗号分隔)。 不想改配置的话,把代理 DNS 改成 redir-host,或把要访问的域名加进 `fake-ip-filter`。 **端口冲突。** 前端默认 5273、后端 8000,通过 `AGENTLAB_WEB_PORT` / `AGENTLAB_PORT` 修改。 **改了后端代码,正在跑的运行显示中断。** `dev.sh` 开着热重载,改一个后端文件就是一次 服务重启。运行会被挂起(不是取消),断点完好:在运行详情页点「接着跑」,跑完的节点不会重跑, 后面的人工审批照常停下来等人。 **运行被中止,提示超过时限。** 单次执行(两次人工介入之间的那一段)默认最多 600 秒, 单次模型调用默认 300 秒没有输出即超时。多半是模型或工具没有响应;确实需要更久,调大 `AGENTLAB_MAX_RUN_SECONDS` / `AGENTLAB_MODEL_TIMEOUT_SECONDS`。设置页「运行环境」里能看到当前值。 **运行报「走满了 N 步还没跑完」。** 循环按自己 `max_iterations` 声明的轮数另算步数预算, 配多少轮就能跑多少轮,不受全局上限约束。走到这条报错的,多半是分支连回上游、却没有 loop 节点给它定轮数上限的环:用 loop 节点包住它。确实需要更多步,调大 `AGENTLAB_MAX_GRAPH_STEPS`(默认 200)。 **agent 没查完就收了尾。** 时间线里有一条提示写明原因:步数用满、连续几步拿不到新信息、令牌或金额预算用完、 上下文接近窗口。按提示调节点上或设置里的步数、预算(预算可以设成不限),见 [agent 的护栏](#agent-的护栏取代固定步数)。 --- ## 开发与验证 ```bash # 后端:约 6600 个测试,-n auto 并行约 40 秒 cd backend && env -u ANTHROPIC_API_KEY -u ANTHROPIC_BASE_URL -u ANTHROPIC_AUTH_TOKEN -u OPENAI_API_KEY \ python -m pytest -n auto # 前端类型检查 cd frontend && npx tsc -b # 前端检查:28 项,默认 8 道并行。连 5273 / 8000,先起 ./scripts/dev.sh node scripts/check-all.mjs # 另起 2 套只给检查用的「后端 + vite」,各用一份数据拷贝,约 6.5 分钟;边改代码边跑也不被打断 CHECK_DATA_SRC=<数据目录> node scripts/check-all.mjs --stacks 2 --lanes 12 # 只跑某几项(名字去掉 check- 前缀) node scripts/check-all.mjs evidence publish ``` pytest 前面的 `env -u` 不能省:没配 Key 的 provider 会退回环境变量里的密钥,启动时的 seed 也会拿它种一个 provider——环境里留着真密钥,测试就可能真的去调接口。 `check-all` 的 28 项按 `scripts/check-all.mjs` 里 `ORDER` 的顺序排: | 类别 | 检查 | |---|---| | 不开浏览器 | `tokens`(令牌静态扫描)`copy`(界面文案)`trace`(运行态内核的纯函数)`decode`(事件翻译层) | | 走线、组件与护栏 | `canvas-layout` `ui-kit` `guards` | | 外壳、回答流与运行态 | `shell` `stream` `evidence`(可点击证据)`run-states`(节点卡)`canvas-fx`(画布表层) | | 各页面 | `studio` `publish`(发布前检查与自动修复)`chat` `runs` `manage` `versions`(上传表格的版本)`catalog`(数据目录)`ui`(端到端,连真数据) | | 复核套件与导出 | `schedules`(定时任务)`compare`(运行对比)`verify`(双盲复算、反方审稿)`inquiry`(审批质询)`challenge`(反方审稿)`bundle`(可验证报告)`whatif`(数字推演)`report-export`(报告导出) | 完整输出写进 `/tmp/agentlab-checks/<时间>/`,终端只给汇总;并行时没过的最后单独重跑一遍,重跑才过的单列成「偶发」。 `--stacks` 每套拷一份数据、端口自动挑空闲的,后端不热重载、vite 不监听文件,跑完、出错或被中断都收干净。 `scripts/e2e-check.mjs` 真发起一次运行(Mock,不花钱),会在库里留下记录,所以不在 check-all 里。 三层前端验证的分工:`check-decode` 守「翻译对不对」,`check-stream` 守「画出来对不对」, `check-ui` 守「整条路走得通不通」。三者可各自通过而合起来是坏的——例如 Step 字段全部正确, 组件却将其渲染为一屏转义 JSON。 `check-canvas-fx` 守的是「事件对了但界面没跟上」:把脚本化的事件灌进 store(走的是 页面上那一份 applyEvent / NodeCard / FlowEdge,只有事件来源是脚本),断言协作矩阵 真的长出来了、并行度算对了、条形长度和耗时成正比、分支命中的出口被点亮、系统关了 动效时光效确实不绘制。真跑一张多 agent 的图要连模型、要等十几秒,而且撞不上 "三个人同时还在跑"那一瞬间——那样的检查会变成偶发失败。 `check-canvas-layout` 单独守「图好不好看」里唯一能算的那部分:它把夹具图交给后端 `/api/copilot/layout` 排版,再直接 `import('/src/canvas/routing.ts')` 用页面上正在跑的那份 走线代码算路径,然后按几何断言——没有两条线压在同一段上、没有线从别人的卡片中间穿过去、 每条线的两端都接在节点边缘。连线叠成一团时 `check-ui` 和 `e2e-check` 一个字都不会说。 ## License This project is **source-available** and is licensed under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0). ### Noncommercial Use You may use, study, modify, experiment with, and develop upon this software for purposes permitted under the PolyForm Noncommercial License 1.0.0. Typical permitted noncommercial uses include: * personal research; * learning and education; * experimentation; * evaluation and testing; * hobby projects; * noncommercial software development. See the [`LICENSE`](./LICENSE) file for the applicable license terms. ### Commercial Use **Commercial use is not permitted without prior written authorization from Yilun JIANG.** If you intend to use this software in a commercial product, commercial service, SaaS offering, customer project, commercial deployment, or other commercial context, you must obtain a separate commercial license before doing so. For commercial licensing, see [`COMMERCIAL-LICENSE.md`](./COMMERCIAL-LICENSE.md) or contact: **Yilun JIANG** **[me@thearchyhelios.com](mailto:me@thearchyhelios.com)** ### Copyright Copyright (c) 2026 Yilun JIANG. All rights reserved except as expressly granted under the applicable license.