把数据库伪装成文件系统:一个业务 Agent 只需要 10 个工具
业务 Agent 的关键不是堆更多工具,而是把数据库、查询和动作收敛到模型已经熟悉的语义里。
大部分团队做业务 Agent 的第一反应是:每个业务动作定义一个工具。查用户、拉订单、跑报表、触发审核……工具列表越来越长,模型选错率越来越高,团队开始怀疑是不是该换个更强的模型。
其实不是模型的问题,是工具面的问题。
我最近做了一个业务系统的 Agent 平台,全程由 Agent 写代码,我没有手写一行。这篇讲几个最值钱的设计决定。
一、工具面:最后只剩 10 个工具
我一开始也定义了几十个工具,后来几乎全删了,最终只保留 10 个:
List / Read / Grep / Edit / Write / Skill / QueryData / WebSearch / WebFetch / RunWorkflow。
核心的一步是:把数据库投影成一个虚拟文件系统。Agent 看到的是 /users、/orders、/reports 这样的路径。List /users 返回用户列表,Read /users/123 返回详情,Grep /orders keyword 做全文搜索。背后没有真实文件,全是 Postgres 查询。
为什么这么做?
因为 List / Read / Grep / Edit 是模型在海量 agent 轨迹里训练过千万遍的动作。它天然知道“先 List 再 Grep 再 Read 再 Edit”怎么组合。你自造一套 fetchUserProfile 式的工具,模型得现学你的语义,选错率和试错轮次都会上升。
工具面设计的最大杠杆,是借模型的先验,而不是教模型你的业务。
领域知识放进文档正文让模型自己读,不要塞进工具 schema 让模型背。
平铺工具还有一个更隐蔽的代价:每加一个工具,都是一次全局语义回归。新工具会和已有工具产生成对歧义。search_videos 和 list_user_videos 的边界在哪?模型会不会在该用 A 的地方调了 B?N 个工具有 N² 量级的潜在混淆,每次新增都得人肉重审一遍,而且出错没有报错,只有 agent 行为悄悄变差。
虚拟文件系统没有这个问题:加业务功能 = 加路径和数据,工具语义一个字不用动。无论系统里有 10 张表还是 200 张表,List / Read / Grep 的含义不变。工具永远是 10 个,语义合同从第一天起就冻结了。
二、万能查询出口:让 Agent 直接写 SQL
业务需求会无限增长,但增长不该变成新工具。
QueryData 让 Agent 直接写 SQL 查库。文件系统语义解决了“读文本”,但业务库里更多的是数字和结构化数据。QueryData 允许 Agent 对整个业务库自由写 SQL 做聚合、对比、排序,只读事务、强制 LIMIT、敏感字段脱敏。
“最近哪类内容涨得快”“哪些高优任务还没处理”——不需要提前做报表,Agent 当场写 SQL、当场出结论。一个工具,顶掉一整个报表需求池。
明天要“按地区筛用户”“统计处理缺口”?那是 SQL 的组合,不是新工具。
三、写操作怎么办:没有一笔直写数据库
写操作分两类,共同点是没有一类允许 Agent 直改数据库。
文本的写是 Edit 和 Write。它们看起来在改“文件”,实际落的是版本表:Edit 只能产生新版本,不能覆盖原文;Write 只能创建新草稿。每一笔写入带版本、带来源,可控、可回滚、可追溯。Agent 会犯错不是异常,是设计前提。所以数据的“正文”永远不暴露给 Agent 直改。
动作的写目前收敛到 RunWorkflow——渲染、批量同步、数据入库全走这一个入口,强制过人工审核。坦白说这还是临时方案,但方向已经想清楚了:把“动作”也变成文件树里的对象。
思路是:/workflows/render-job 可以 Read,正文写着它做什么、要什么参数、有什么风险;要执行,就 Run 它。跟 Unix 一样,数据是文件,可执行程序也是文件。
这样一来,新增一个业务动作 = 往树里放一个东西,不是加一个工具。Agent 靠 List 发现有什么能跑,Read 看说明,Run 执行。这其实就是渐进式披露:工具 schema 里不预装任何业务能力,Agent 走到哪、发现到哪。系统能力可以一直长,工具还是那 10 个。
四、执行引擎:一千行代码,零依赖,谁都能用
整个 agent 循环被抽成一个独立的包,约一千行。我没用任何 SDK 或框架,不是洁癖,是真找了一圈没有合适的:模型调用层的 SDK 太薄,没有持久化会话的概念;agent 框架又太厚,循环和状态被框架拥有,我的规则塞不进去。
引擎只管两件事:循环怎么转,模型每一轮看到什么。其余一概不管:数据存哪、模型调谁、接什么业务工具,全部是外面传进来的。
所以它是无状态的。每次调用,给它当前会话日志加一组适配器,它推进几步、吐出新条目,结束。状态永远在日志里,不在引擎身上。崩溃恢复、精确重放、随时中断,都是这一个设计的自然结果。水平扩容也是免费的,任何实例都能接着任何会话继续跑。
想换存储、换模型、换宿主,后端服务、CLI、定时任务,换一组适配器就行,引擎一行不动。它是库,不是框架。框架要求你围着它长,库嵌进你已有的系统里。
五、一个贯穿全文的道理
回头看这些设计,其实都在做同一件事:借模型已经会的东西,而不是教它新东西。
工具面用文件系统语义,因为模型练过无数遍。数据查询用 SQL,因为模型写 SQL 比调你自定义的 API 准得多。连写操作也塞回文件树里,因为 List / Read / Run 同样是模型最熟的操作。增长永远压进已有的工具合同里,而不是每次加新工具让模型重新学。