糖豆(tangdou)加载本体规则
1. 本体规则的定义与界面编辑
什么是本体规则
本体规则(Ontology)是糖豆 Text2SQL 系统的"领域知识库",用于告诉 AI 助手数据库的结构、表关系和业务查询规则。本体规则定义在 backend/ontology/ 目录下,包含两个 Markdown 文件:
| 文件 | 用途 | 内容概要 |
|---|---|---|
schema.md | 数据库结构概述 | 9 张表的名称、主键、外键、用途说明,表关系 ER 图,查询策略提示 |
rules.md | 业务查询规则 | 区划/部门的 TYPE 和 LEVEL 取值含义、报表/任务表的分组字段说明等 |
通过界面修改
糖豆提供了可视化的本体管理界面(前端 Ontology 系列组件),用户无需手动编辑文件:
- 表结构面板(
OntologyTableListPanel):查看和编辑表的中文名、主键、外键、用途 - 字段详情面板(
OntologyDetailPanel):管理每张表的字段(名称、类型、备注) - 表关系图(
OntologyGraphPanel/OntologyRelationPanel):可视化展示表之间的外键关系 - 规则编辑器(
OntologyRulesEditor):在线编辑业务查询规则
当用户在界面中修改规则时,后端 API(PUT /api/ontology/rules)会同步做两件事:
- 写入数据库
ontology_rules表(持久化存储) - 写入
backend/ontology/rules.md文件(供 Agent 加载)
用户编辑规则 → PUT /api/ontology/rules
├→ 写入 ontology_rules 表
└→ 写入 ontology/rules.md ← Agent 热重载入口这一设计使得规则修改后,Agent 能在下次请求时自动加载最新内容,无需重启服务。
2. 早期方案:create_deep_agent + HotReloadMemoryMiddleware
架构概述
糖豆早期使用 deepagents 库的 create_deep_agent 创建 Agent 实例。该框架内置了 MemoryMiddleware 机制,用于在 Agent 运行时动态加载本体文件。
工作流程
用户请求
↓
deepagents Graph 循环
├─ agent 节点 → [MemoryMiddleware 拦截]
│ ├─ 检查 ontology/*.md 的 mtime
│ ├─ mtime 变了 → 重新读取文件内容
│ └─ 注入到 agent state 的 memory 字段
├─ tools 节点 → 执行 SQL 工具
└─ agent 节点 → [MemoryMiddleware 再次拦截]
└─ 每次节点转换都检查 mtime热重载原理
HotReloadMemoryMiddleware 在每次 agent 节点转换时执行:
- 遍历
ontology/目录下的所有.md文件 - 对每个文件调用
os.path.getmtime()获取修改时间 - 与缓存的 mtime 对比
- 如果检测到变化,重新读取文件内容并注入到 agent 的 state 中
- LLM 在下一次调用时就能看到最新的本体内容
问题
这套机制虽然功能完整,但存在显著的开销:
- 每步执行:agent 循环中的每一次节点转换都执行 mtime 检查和文件读取
- 额外注入的中间件:deepagents 还自动注入了 6+ 个中间件(Filesystem、SubAgent、TodoList、Summarization、PatchToolCalls、Skills),每个中间件都有运行时开销
- 额外注入的工具:自动添加了 8+ 个糖豆不需要的工具(ls、read_file、write_file、edit_file、glob、grep、task、write_todos)
- 额外的 Token 开销:deepagents 的 BASE_AGENT_PROMPT(约 2000 字符)和 SKILL.md 文件(约 6000 字符)每次 LLM 调用都会被发送,而糖豆的 Text2SQL 场景完全不需要这些
3. 当前方案:create_react_agent + Prompt 直接嵌入
架构概述
切换到 langgraph 官方的 create_react_agent 后,本体内容在 Agent 创建时直接嵌入 prompt 参数,不再依赖运行时中间件。
工作流程
Agent 创建(惰性初始化或热重载时)
↓
load_prompt_and_mtimes(base_dir)
├─ 读取 AGENTS.md
├─ 读取 ontology/schema.md
├─ 读取 ontology/rules.md
└─ 拼接为完整 system_prompt
↓
create_react_agent(model, tools, prompt=system_prompt)
↓
用户请求
└─ agent 循环(无中间件开销)
├─ agent 节点 → LLM 调用(prompt 中已含本体内容)
└─ tools 节点 → 执行 SQL 工具热重载原理
虽然 create_react_agent 不支持自定义中间件,但热重载能力通过 Agent 实例级别的 mtime 检查 实现:
在 ChatService._get_agent() 中,每次请求都会检查本体文件的 mtime:
python
def _get_agent(self, provider: str):
agent = self._agents.get(provider)
_, current_mtimes = load_prompt_and_mtimes(self.base_dir) # 获取当前 mtime
cached_mtimes = self._agent_mtimes.get(provider)
# mtime 未变 → 直接复用缓存的 agent
if agent is not None and cached_mtimes == current_mtimes:
return agent
# mtime 变了 → 重建 agent(新 prompt 生效)
with self._agent_lock:
agent = create_agent_instance(
provider, self.base_dir, checkpointer=self._checkpointer,
)
self._agents[provider] = agent
self._agent_mtimes[provider] = current_mtimes
return agentload_prompt_and_mtimes 函数内部带有 5 秒 TTL 缓存,避免每次请求都执行 os.path.getmtime 系统调用:
python
_MTIME_CACHE_TTL = 5.0 # 秒
def load_prompt_and_mtimes(base_dir: str) -> tuple[str, dict[str, float]]:
# TTL 缓存:5 秒内复用上次的 mtime 结果
now = time.time()
if now - _mtime_cache_ts > _MTIME_CACHE_TTL:
_mtime_cache = {}
for fpath in files:
_mtime_cache[fpath] = os.path.getmtime(fpath)
_mtime_cache_ts = now
# 组装 prompt:AGENTS.md + ontology/*.md
...
return prompt_text, dict(_mtime_cache)热重载完整链路
用户在界面修改规则
↓
PUT /api/ontology/rules
├→ 写入 ontology_rules 表
└→ 写入 ontology/rules.md ← 文件 mtime 更新
↓
用户发送下一条消息
↓
ChatService._get_agent()
├→ load_prompt_and_mtimes() 读取 rules.md 的新 mtime
├→ 对比缓存的 mtime → 发现不一致
├→ 重建 agent(新 prompt 包含最新规则)
└→ LLM 调用时使用最新的本体内容4. create_react_agent 与 create_deep_agent 的主要区别
本质定位
create_react_agent | create_deep_agent | |
|---|---|---|
| 来源 | langgraph.prebuilt(官方内置) | deepagents(第三方扩展库) |
| 设计目标 | 极简 ReAct 循环 | 通用自主代理框架 |
默认注入的工具
| 工具 | react | deep | 说明 |
|---|---|---|---|
| 用户自定义 tools | 有 | 有 | SQL 工具、图表工具等 |
| 文件系统(ls/read/write/edit/glob/grep) | 无 | 自动注入 | 糖豆不需要 |
| 子任务委派(task) | 无 | 自动注入 | 糖豆不需要 |
| 待办列表(write_todos) | 无 | 自动注入 | 糖豆不需要 |
默认注入的中间件
| 中间件 | react | deep | 作用 |
|---|---|---|---|
| FilesystemMiddleware | 无 | 自动注入 | 给 LLM 提供虚拟文件系统 |
| SubAgentMiddleware | 无 | 自动注入 | 允许 spawn 子 agent 处理子任务 |
| TodoListMiddleware | 无 | 自动注入 | 自动管理任务清单 |
| MemoryMiddleware | 无 | 自动注入 | 热重载 ontology/skills |
| SummarizationMiddleware | 无 | 自动注入 | 上下文超长时自动摘要 |
| PatchToolCallsMiddleware | 无 | 自动注入 | 修复 LLM 不规范的 tool_call 格式 |
API 参数
python
# create_react_agent — 5 个参数
create_react_agent(model, tools, prompt, checkpointer, debug)
# create_deep_agent — 10+ 个参数
create_deep_agent(model, tools, prompt, base_dir, middleware,
skills, filesystem, checkpointer, ...)每次 LLM 调用的 Token 开销对比
react: AGENTS.md(~1KB) + ontology(~2.6KB) + 用户消息 ≈ 4KB
deep: BASE_PROMPT(~2KB) + AGENTS.md + SKILL.md(×2 ~6KB)
+ ontology + 中间件注入的 state 描述 ≈ 12-18KB热重载对比
| 维度 | Middleware 方案 | Prompt 方案 |
|---|---|---|
| 检查时机 | 每次节点转换(agent→tools→agent 循环中的每一步) | 每次用户请求(进入 _get_agent 时) |
| 检查频率 | 高(多轮对话中每步都检查) | 低(每请求检查一次,带 5 秒 TTL 缓存) |
| 生效时机 | 下一步立即生效 | 下次请求生效 |
| 运行时开销 | 每步执行中间件逻辑 | 零(prompt 在创建时固定) |
为什么选择 create_react_agent
糖豆是 Text2SQL 场景,只需要 SQL 工具和图表工具:
- 不需要文件系统(没有代码要读/写)
- 不需要子任务委派(单步 SQL 查询够用)
- 不需要待办列表(查询-执行-图表是线性流程)
deep_agent 自动注入的 8+ 个工具和 6+ 个中间件全是无用开销。切换到 react_agent 后:
- 移除了无用工具和中间件的运行时开销
- 每次 LLM 调用减少约 8000+ 输入 Token
- 保留了热重载能力(通过 Agent 实例级别的 mtime 检查)
- 移除了对 deepagents 第三方库的依赖