大模型进业务系统,第一道关不是模型,是权限
选模型、写提示词,这两件事在 2026 年已经不是门槛。门槛在后面:这个助手能看哪些数据、能替访客提交什么、越界了怎么拦。这一层做在应用里还是做在服务端,决定了后面能不能换模型。
我把这一层做进自己站上的整个过程里,读出来的判断全在代码里。这里不是教程,是一组能对着自己那套系统逐条核的判据。
读侧:模型能看什么,收口在工具而不是提示词
我的站内助手只给模型两格工具。read_post 读一篇本站文章的正文,record_reservation 替访客递一条预约。没有"搜索互联网"这一格,没有"执行任意查询"这一格。能读的范围收在工具返回值里,而不是模型自己决定去哪取。
read_post 走的是本站的仓储层,拿回来的就是已发布的内容,一次最多取回六千字。遇到一篇带访问口令的文章,工具直接回一句"需要口令才能看,本站助手不代读带口令的内容"——这句话是 Java 代码里的 if 分支,不是提示词里的自然语言指令。换一家模型,这一格 if 照样在那,口令内容照样拿不到。
上下文另外有一道收口:每次问话只取最近十句喂进模型,不会因为第 20 轮还在为第 1 轮的事付 token。这一道由 CONTEXT_TURNS 那个常量在代码里写死,不由提示词去描述"你只应该看最近几轮"。
提示词里有一句"你能依据的只有这两份材料",但那句是给人看的。真正约束模型输出范围的那道墙,物理上立在服务层:工具定义决定了它能调什么,工具返回值决定了它拿到什么,模型能用的就只有这些。
判据一:把提示词里那句"不要回答 XXX"删掉之后,你的助手还能不能拿到那些数据?如果还能拿到,边界就只在提示词里——换一家模型,那句话不一定还管用。
写侧:助手代提交与表单走同一条路,来源另起一格
助手能替访客做一件"动手"的事:递一条预约进站长队列。
这一格工具走的是 ReservationCommandService.submit——与页面那张表单同一个入口,于是五道守卫一道不少:站点开关、时段取值、手机号格式、来源页、同地址间隔。不另开一条路,因为少一道守卫的第二个入口,等于给刷量的人留了一条"表单有限速、对话没有"的侧门。
《业务系统里的 Agent 这个词,早就有主了》(/archives/agent-word-already-taken.html)里讲"模型不能碰写"那条边界,是单据系统的说法;本站没有审批流,思路一致:助手的写必须穿过同一个服务入口,不享有特权路径。
关键在落库那一下。cms_reservation 有一列叫 source,取值只有两个:form 和 assistant。这一格不从请求里读。ReservationSubmitContext 的注释说得很清楚:客户端递一个 source=assistant 只会把数字做假;它写的是调用方自己——表单控制器硬编码 FORM,助手那格工具硬编码 ASSISTANT,两处各自硬编码,服务层只是把它抄到行上。
conversationId 同样不由请求方填。表单那条路传 null,NULL 在库里说的是实话:这一条前面没有对话。助手那条路把当次对话请求自己的会话标识抄进去,站长在后台点开这一条,能看到他留号之前问过什么。这一格的价值在一个数字上:没有它,那条闭环就只能证明它跑得通,证明不了它有人走。
判据二:你的系统里,助手代替人写下去的那条记录,能不能从后台数据里直接分辨出来?如果它和人的操作共用一个无标记的写入路径,上线之后就没法评估这条路有没有人走。
留档:前台的出参和后台的出参,是两套形状
三个类各管一件事,合起来就是留档的形状。
| 类 | 用在哪 | 包含字段 |
|---|---|---|
ChatReplyVO |
这一次问答的回包 | 正文、时间戳 |
ChatTurnVO |
历史接口的每条 | 说话方、原话、时间戳 |
ChatRecordVO |
后台对话记录 | 以上三样,外加会话标识、来源地址、工具调用记录 |
ChatReplyVO 不回上下文、不回访客自己那句话——那两样调用方本来就有,回一份等于在同一条响应里放两份文本。ChatTurnVO 不带会话标识,那一次查询的参数里本来就有它。
ChatRecordVO 的注释写的是:"这两格都不该出现在前台响应里。"来源地址挂在访客出参里,等于多一处暴露访客身份的地方;toolCalls 是助手这一轮用了哪些工具的记录——只有键名,不含取值,因为访客口述给助手的手机号属于工具入参,它该出现在预约表里,不该在会话台账里再留一份副本。站在窗口前的访客看不见它,它是站那一头的账。
这里有一条架构判断:出参形状就是权限边界。如果前台和后台共用一个 VO,边界就只靠控制器层的运行时过滤;分成两套类,编译期就把"给了什么"写死了。
判据三:你的对话记录有没有两套出参——一套给访客、一套给后台——且两套的字段差集里有会话标识、工具调用记录、来源地址这些"站那侧的账"?没有就把这一格补上,补上之后换模型时台账的连续性不跟着断。
这一层做在哪里,决定能不能换模型
助手对话的入口 /api/ai/chat 落在鉴权过滤器的公开前缀里,任何人都能问——能不能问、能问几句、隔多久能再问,全在服务层那四道守卫里判:助手开关、来源页、敏感词、当日额度。后台的对话记录读取挂在 /api/admin/chats 下,由前缀统一要求登录态,与访客入口物理分开。过滤器管"要不要登录态",业务闸门管"这一句收不收"——两件事不混在一起,这是这道关做在服务端的第一个落点。
当日额度按来源地址而非按会话算:会话标识是浏览器随便生成的,换一枚就换一个计数桶,按会话算等于换一个本地存储就解锁一次。这条判断不在提示词里,在 guardQuota 那一段查询里。把权限做成"整条链路只有一个门"、那个门用提示词来表述的系统,等于把它交给模型自行执行——换模型即换执行者,执行质量跟着变。把门分在过滤层和业务层两级,两级都是代码,换谁都换不掉。
把读侧、写侧、留档三节合起来,边界住在服务层还是不住在提示词里,可以逐条核:
- 读侧的拒绝分支在工具实现里——
postText()那个if在换模型之后照样挡住口令内容,不依赖模型是否"理解"了那句禁止。 - 写侧的来源标记由调用方硬编码——
ReservationSourceEnum那一格不受请求影响,换模型之后后台的分辨力不丢。 - 前台出参和后台出参是两套类——新模型的回答照样落进同一张
toolCalls列,台账结构不变。 - 当日额度按来源地址而非按会话算——换模型不换入口,刷量的上限还锁在同一条查询里。
《Agent 上生产线:一半公司已跨过,卡在哪》(/archives/agent-production-barrier.html)里讲权限那一节,说的是"Agent 拿到的是谁的凭据";这篇讲的是那格凭据的范围在代码里怎么落——一个是身份层,一个是边界层,两件事不同角度。
网关那一篇(/archives/llm-gateway-not-model-choice.html)讲了路由和失败转移;它的前提是这一层已经做完,才谈得上随时可以换。如果"能读什么、能写什么"全靠提示词,换一家模型就要重新测那段提示词——那就不叫随时换,叫每次换都要重审这道关,没人审得起。
一句收尾
模型选型是采购问题,权限边界是架构问题。先把读侧收口立进工具返回值、写侧与表单共用同一条入口并单独标记来源、留档出参按前台后台分成两套形状——这三件事落在服务层,换模型才换得掉。
本文作者:Aryee 发布时间:2026-09-27 13:26
本文出处:ARYEE.cn 固定链接:https://www.aryee.cn/archives/ai-permission-before-model.html
转载或引用请连这一行一起带走;文中的代码与结论按当时环境成立,组件升级后请以官方文档为准。