智能问数 · 架构设计
整体架构
用户在对话页提问。网关先绑定会话和数据源,再调用模型把问题拆成任务;每个查询都经过护栏,交给网关进程之外的执行器跑;结果整理成素材后再由模型写回答。评估台只读网关落盘的链路文件。这里先看系统关系,具体组成见核心交付。
可左右滚动查看完整图;点击图可全屏缩放。
部署架构
面向一台笔记本,不要求 GPU。数据用可重现的合成库。本地 start.ps1 同时拉起网关、对话页和评估台;容器部署时只有前端发布端口,网关留在 compose 网络内。
| 组成 | 位置与端口 | 作用 |
|---|---|---|
| 对话页 | frontend/,本地 5179 | 登录、选源、对话、数据集、Skills、本域账号 |
| 网关 | backend/,本地 8000 | 只提供 API;聊天用 SSE;容器里只 expose,不发布端口 |
| 评估台 | review/,本地 5181 | 管理员登录后按链路版本查看证据;不调模型、不执行 SQL |
| 容器入口 | 前端镜像,默认 127.0.0.1:8080 | Nginx 提供页面并反代 API;健康检查打网关 /live |
| 模型 | TTS_LLM_BASE_URL 指向的兼容接口 | 拆解与写回答;本地实测用的是 qwen3.8-flash |
| 执行路径 | 怎么跑 | 隔离形态 |
|---|---|---|
| 编译查询对 SQLite / 文件快照 | 独立子进程,PRAGMA query_only = ON,带超时 | 子进程 |
| PostgreSQL 业务源 | pg_worker 子进程,连接 3 秒、语句 5 秒超时,只读事务 | 子进程 |
| 模型生成的 Python | 固定 @sha256 摘要的镜像;无摘要、无 Docker 或来源是 PG 时直接拒绝 | 容器 |
| 闭集计算 | 留在网关进程内,只对已接受结果做允许的运算 | 不执行代码 |
Python 容器的固定参数:--network none、--pull never、--read-only、/work 与 /tmp 为 noexec tmpfs(64 MB / 16 MB)、--memory 256m、--cpus 1、--pids-limit 64、数据文件以 :ro 挂到 /data/app.db、以 65534 用户运行 python -I。默认超时 10 秒,输出截断在 64 KB。
请求链路
下面这张图比整体架构多展开一层:一轮里模型被调用两类,拆解一次、写回答一次;中间每个查询单独过护栏。虚线回到对话页的是用户会看到的事件。
可左右滚动查看完整图;点击图可全屏缩放。
- 一次提问:对应一次
POST /v1/chat,生成一个新的run_id。 - 小轮预算:
TTS_REQUEST_TIMEOUT_MS默认 60 秒,管每一次模型 HTTP 调用。 - 整轮墙钟:
TTS_CHAT_WALL_TIMEOUT_MS默认 20 分钟,管同一次提问。网关用单调时钟记下截止时间。 - 怎么扣时间:拆解、回退回路里的每步模型调用、五阶段写回答,各自取小轮和墙钟剩余里更短的那个。工具和查询波次只受墙钟,以及执行器自己的超时约束。模型出流不会把小轮或墙钟清零重算。
- 执行器超时:SQLite 查询默认 5 秒。PostgreSQL 连接默认 3 秒,语句默认 5 秒。Python 容器默认 10 秒。
- 提前结束:断连、换源或中断按钮结束本轮。已经产生的副作用不会自动再执行。
- 回退工具回路:意图解析不出,或表结构不是 SQLite 文件时,模型只能看到
get_schema、sample_data、plot、export、run_python,最多 10 步。run_sql和confirm登记在同一张表里,模型看不见,只有受信任调用方能用。
关键设计
一轮问数怎么走到回答
可以按五步理解一次问句:
- 拆解:模型把原问拆成带依赖的 query / compute 任务,每项有业务问题、完成条件和编号;服务端校验覆盖、依赖和任务边界。
- 等待:检查时间范围、指标、粒度、过滤条件是否会改变答案。有歧义就持久化问题和候选,发等待事件,依赖该决定的任务不启动;用户回答后生成新的计划修订,只重校验受影响任务。
- 执行:query 走「意图 → 编译 → 只读执行」,模型不填写裸 SQL;compute 只允许加减乘除、比率、增速、求和、平均、排名和 top-N,空值策略和精度写在规则里。每个任务记录尝试、结果和错误码。
- 整理:只合并已接受的结果,按确定规则处理重复、单位、空值和缺项;每条事实指回产生它的任务、结果和字段。
- 回答:模型只读素材,每个数字绑定事实编号;对不上的数字标为未验证,不编造补齐。
可左右滚动查看完整图;点击图可全屏缩放。
实现上,analytics/run_plan.py 决定进五阶段还是回退工具回路;analytics/pipeline.py 负责等待、查询、计算、素材和回答;analytics/task_runner.py 把 IntentQuery 编译后送进只读子进程;analytics/compute.py 定义闭集运算。终态只用 completed、partial、failed、cancelled、expired 五个词,运行中的等待和阻塞是阶段或任务状态,不直接当成给用户的结果。
护栏怎么判
护栏是一个独立 Python 包,运行期只依赖 Pydantic,公开符号是 Guard、Principal、ExecResult、ErrorCode、GuardPolicy、AuditSink。它接收调用方已经构造好的身份,不解析令牌,不调用模型。
- SQL:只允许
SELECT或WITH开头;禁止ATTACH/DETACH/PRAGMA;表名必须在本来源的白名单里。每条拒绝带规则编号,例如SQL-START-01、SQL-ATTACH-01。 - Python:静态检查采用允许清单,不是黑名单。允许
math、statistics、json、datetime、decimal、itertools、collections、re、pandas、numpy等;拒绝eval/exec/__import__、open/input、getattr系列和__class__.__bases__.__subclasses__这类属性链;代码超过 8000 字符直接拒绝。错误码分DANGEROUS_IMPORT、DANGEROUS_CALL、DANGEROUS_ATTRIBUTE。 - 审计:每次放行或拦截记录身份、动作、决定、命中的规则、SQL 或代码摘要和耗时;浏览器收到的事件里不含 SQL 原文和工具参数。
- 低信任内容:工具返回、表结构注释和工具描述先标记信任级别,再决定能否进入后续指令。
副作用和确认
export 这类有副作用的动作在回路里先挂起,网关返回确认卡片;用户通过 POST /v1/confirm 批准,服务端校验身份和参数摘要后执行一次,重复提交幂等。viewer 不能批准。模型如果自己发出 confirm 调用,按未登记工具处理。
会话与数据源
每个对话是一个工作区,服务端只认工作区里已绑定的 source_id,未绑定就发问返回 SOURCE_REQUIRED。换源会清空上一轮上下文。数据源有三类:操作者用 CLI 登记的 SQLite 文件或 PostgreSQL 库、用户自己导入的 CSV / SQLite 快照(单文件 8 MB、2 万行、保留 5 个版本)。公司之间靠登录会话隔离。
运行观测
GET /live:进程能响应,不需要令牌。GET /ready:配置加载成功即就绪;没有 Docker 时在响应里说明沙盒不可用,不算失败。GET /v1/eval/traces、/v1/eval/traces/{run_id}:管理员按数据源读取链路,供评估台使用。- 每轮落盘
runs.jsonl(步数、token、停止原因)、chains.jsonl(五阶段事件,chain_schema_version: 2.0)、transcript.jsonl和taskboard.json;护栏审计另起一条流。
系统边界
- 五阶段的入口有条件:意图能解析、且表结构来自 SQLite 文件时才进入;PostgreSQL 来源目前走回退工具回路。对话页还不处理澄清恢复事件,
POST /v1/clarify只能从接口触发。 - 必需任务失败:必需任务失败会结束本轮并返回错误,未完成的任务不标成成功。
- 身份:本地和演示用会话登录;护栏只接收已构造的身份。主产品若接入,由它完成真实鉴权。
- 数据:合成数据,可重现;不使用公司客户数据和内部表结构。