

Day 6 — 2026-03-25#
从原型到产品。这一天把认证、数据库、权限、Pipeline 几个”迟早要做”的事一口气补齐了。
做了什么#
这一天的主题是生产就绪。之前 Forge 是一个能跑的原型——单进程、SQLite、无认证、无权限。今天把它变成一个可以真正私有化部署的服务。
六件事:PostgreSQL 支持、认证鉴权、数据权限、Pipeline E2E 打通、Web Admin 完整落地、EA 准确率优化。
一、数据库抽象层:SQLite → PostgreSQL#
第一个坑出现得很快。
原来的代码直接用 SQLite 的原生 API:? 占位符、connection.lastrowid、BOOLEAN DEFAULT 1、一个大事务里跑所有 DDL。换 PostgreSQL 的时候全部炸了。
不一样的地方:
-
占位符:SQLite 用
?,PostgreSQL 用%s -
自增主键:SQLite
lastrowid,PostgreSQL 要RETURNING id -
BOOLEAN 默认值:
DEFAULT 1在 PostgreSQL 是 error,要DEFAULT TRUE -
DDL 事务:PostgreSQL 的
CREATE TABLE IF NOT EXISTS如果表存在会报错中断整个事务,SQLite 不会
解决方案是 _UnifiedConn 包装器——对上层代码暴露统一接口,屏蔽方言差异。DDL 执行改成逐条提交,遇到 “already exists” 直接跳过。
# 修复前:一个事务跑所有 DDL
with engine.begin() as conn:
conn.execute(text(ddl))
# 修复后:逐条事务,"already exists" 不阻断后续
for statement in ddl.split(";"):
try:
with engine.begin() as conn:
conn.execute(text(statement))
except Exception as exc:
if "already exists" in str(exc).lower():
continue
logger.warning("DDL execution warning: %s", exc)plaintext同时把 BOOLEAN DEFAULT 1 在 PostgreSQL 路径上自动替换为 DEFAULT TRUE。
二、认证鉴权#
Forge 的认证需求很简单:Web UI 一个管理员账号,API 可选 token。但要安全——不能直接存明文密码,不能有状态 session。
选了 HMAC-SHA256 签名的无状态 cookie。流程:
登录 → 用 admin_password 对 "user_id:timestamp" 签名 → 写入 httponly cookie
请求 → 验签 + 过期检查(7天TTL)→ 放行plaintext一个容易漏的细节:浏览器通过 /api/chat 发 AJAX 请求时,带的是 session cookie 而不是 API Key header。所以 require_api_auth 要额外检查 session cookie,否则 Web UI 用户登录后调不了自己的 API。
async def require_api_auth(request: Request):
if not cfg.AUTH_ENABLED:
return
if verify_api_key(request): # X-API-Key header 或 ?api_key=
return
if verify_web_request(request): # Web UI session cookie 也算合法
return
raise HTTPException(status_code=401, detail="Unauthorized")plaintext三、数据权限:team 级别 ACL#
数据权限是多租户场景里最容易犯的错。常见的懒做法是在 WHERE 里拼条件——看起来有权限控制,实际上稍微改改 prompt 就能绕过。
Forge 的解法是在信息输入端做过滤:retriever 向量检索时,只从 allowed_tables 里取相关表,LLM 压根看不到被限制的表的 schema。LLM 生成的 Forge JSON 里引用了无权限的表,编译器也会拒绝。
user → agent.process()
↓ 查 team_table_acl 获取 allowed_tables
llm.call(allowed_tables=[...])
↓ retriever 向量检索时过滤
只看到有权限的表 schema
↓ LLM 生成 Forge JSON
编译器不会产出无权限表的 SQLplaintext不需要运行时动态 WHERE,不存在绕过路径。
四、Pipeline E2E#
Pipeline 已经在 agent/pipeline.py 里实现了好几周,但 /api/chat 从来没有路由到它。相当于造了辆车,从来没上路。
这次把两端接上:
-
/api/chat:检测到 analyze/visualize/report 意图时,调pipeline_runner.run() -
/api/approve:SQL 执行完成后,把结果行注入 QueryResult artifact,调runner.resume()
中间有一个有趣的架构问题:Pipeline 在审批环节暂停,等待 SQL 执行结果。但 SQL 执行在 /api/approve 里,两个 endpoint 之间怎么传数据?
解法是 WMB(Working Memory Buffer)。/api/chat 把挂起的 pipeline_run 存到 WMB,/api/approve 从 WMB 取出来,注入执行结果,再 resume。WMB 本来设计给跨轮次状态传递,这里正好用上了。
另一个 bug:StageRun.to_dict() 当 artifact 是普通 dict(从 EMS 反序列化后)时会崩溃,因为它调了 .to_dict() 方法。加了 isinstance 判断修掉了。
五、Web Admin 完整落地#
这一天新增了 6 个 Admin 页面:
| 页面 | 路径 | 功能 |
|---|---|---|
| 登录 | /login | HMAC session 登录 |
| 记忆管理 | /admin/memory | SMP 条目浏览/删除,EMS 统计/清除 |
| 团队管理 | /admin/teams | 创建团队,管理成员,设置表级 ACL |
| 团队成员 | /admin/teams/{id}/members | 成员 CRUD |
| 文档导入 | /admin/knowledge/import | 上传 .txt/.md,LLM 提取知识点,确认入库 |
| 设置 | /admin/settings | Auth 开关 + 密码 + API Keys + Memory DB URL |
Auth 配置和 Memory DB 配置直接写入 forge.yaml,改完即生效。不需要重启,不需要编辑配置文件。
六、EA 准确率:67.5% → 70.0%#
上次 Day 5 的 M2.7 基准是 72.5%(Method R,每题 5 次均值),已经是非常强的数字。
这次加了 Method S——把 Day 5 的 P0 修复(ANTI JOIN scan 必须是主表)单独隔离出来验证效果。三轮均值:70.0%(+2.5pp vs 原始 67.5%)。
提升主要来自 ANTI JOIN 分类:60% → 80%(+20pp)。说明 scan 方向的问题在这个分类里确实是主要错误来源,修复有效。
架构上的一个转变#
做完这一天,Forge 从单用户原型变成了多用户服务。
变化的核心不是功能数量,而是数据隔离边界从代码约定变成了数据库约束。team_table_acl 里没有的表,不会进入任何用户的 LLM context——这是物理隔离,不是逻辑判断。
认证也类似。HMAC cookie 的过期和签名在数学上保证,不依赖应用逻辑的正确性。
这两个设计的共同点:把安全性放在了比业务逻辑更低的层,使得上层代码写错了也不会造成数据泄露。
石头 | 拾穗数据