拾穗数据

Back

Forge 开发实录【day 6】:从原型到可部署的服务|Forge 开发日记Forge 开发实录【day 6】:从原型到可部署的服务|Forge 开发日记

Forge 开发实录【day 6】:从原型到可部署的服务|Forge 开发日记#

“从原型到产品。这一天把认证、数据库、权限、Pipeline 几个’迟早要做’的事一口气补齐了。“


做了什么#

这一天的主题是生产就绪。之前 Forge 是一个能跑的原型——单进程、SQLite、无认证、无权限。今天把它变成一个可以真正私有化部署的服务。

六件事:PostgreSQL 支持、认证鉴权、数据权限、Pipeline E2E 打通、Web Admin 完整落地、EA 准确率优化。


一、数据库抽象层:SQLite → PostgreSQL#

第一个坑出现得很快。

原来的代码直接用 SQLite 的原生 API:? 占位符、connection.lastrowidBOOLEAN 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
编译器不会产出无权限表的 SQL
plaintext

不需要运行时动态 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 页面:

页面路径功能
登录/loginHMAC session 登录
记忆管理/admin/memorySMP 条目浏览/删除,EMS 统计/清除
团队管理/admin/teams创建团队,管理成员,设置表级 ACL
团队成员/admin/teams/{id}/members成员 CRUD
文档导入/admin/knowledge/import上传 .txt/.md,LLM 提取知识点,确认入库
设置/admin/settingsAuth 开关 + 密码 + 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 的过期和签名在数学上保证,不依赖应用逻辑的正确性。

这两个设计的共同点:把安全性放在了比业务逻辑更低的层,使得上层代码写错了也不会造成数据泄露。


石头 | 拾穗数据

Forge 开发实录【day 6】:从原型到可部署的服务|Forge 开发日记
https://blog.ss-data.cc/blog/forge-day-6-forge
Author 石头
Published at 2026年3月27日
Comment seems to stuck. Try to refresh?✨