给一个跑了很多年的系统接大模型,最容易翻车的不是模型答得不好,而是你还没搞清这摊老代码的地基,就急着把 LLM 塞进业务里。这个系列讲怎么把 AI 能力一步步推进存量系统,第一篇不写接入逻辑,先把三件最容易被忽略的事说清楚:数据、部署、测试。
数据与事务
老系统的第一个硬约束是数据。它的表结构可能是十几年前设计的,字段名缩写、类型混乱、大量 NULL,还绑着一堆外键和触发器。你想让模型「读一下订单状态」,先得回答:订单在哪个表、状态字段叫 state 还是 status、脏数据算不算。这些不搞清楚,模型给的每个结论都建立在没核对的字段上。
更麻烦的是事务边界。存量系统里一次下单可能横跨五六张表,中间还有同步调用,甚至靠触发器补数据。如果 AI 要「改一条记录」,它改的那张表很可能只是某个分布式事务的一半,直接写会破坏一致性。软删除是另一个暗坑:很多老系统删数据不是真删,而是把 is_deleted 置 1,模型若不知道这个约定,就会把「已删除」的数据当成有效数据读出来。所以在动手前,先把核心表的主外键关系、写入路径、软删除标记、审计字段都摸一遍,否则模型再聪明,也是在错误的地基上跑。另一个容易被忽略的是字段含义漂移——同一个字段在不同历史时期可能存了不同含义的值,比如早期 status=3 表示「取消」,后来改成「退款中」,模型按现在的枚举去理解老数据就会出错。
运行时与发布
第二个约束是运行时和发布节奏。老系统可能还跑在旧版本的 Java 或 PHP 上,Python 3.8 都算新;发布可能是一个月一次的大窗口,没有灰度,没有 feature flag,回滚要等下一班。这意味着你没法像新项目那样今天改明天上线,任何对老系统的侵入式改动,代价都高得不成比例。
接入 AI 最安全的姿势,是把它做成一个「旁路」:只读老系统,不改老系统。模型读数据、生成结果,写回走一个独立的新服务或异步队列,而不是直接连老库写。这样即使模型那一侧挂了,老系统照常跑,故障被隔离在旁路里,回滚也只需要停掉旁路,不用动主链路。
测试与可观测
第三个约束最容易被低估:老系统往往没有测试。你不敢动它,不是因为它复杂,而是没有回归网兜着。给这样的系统加 AI,一旦模型改动触发隐藏的边界条件,你连「哪儿坏了」都定位不到——日志要么没有,要么是半年前就没人看的格式。
所以接模型之前,先补一层最低限度的可观测:关键路径打点、异常告警,以及一段「只读审计」脚本,把系统现状摸成一张清单。没有这张清单,你后面每一步改动都是在蒙着眼睛改。
摸清底的目的
把数据、部署、测试这三件事摸清,目的不是写一份文档交差,而是给后面每一步改动定边界。数据边界决定了模型能读什么、不能碰什么;部署边界决定了改动怎么上线、怎么回滚;测试边界决定了你敢不敢动。三件事没摸清之前,任何「让模型帮忙改代码」「让模型自动写数据」的念头都是危险的,因为你连失败了怎么收场都不知道。反过来,摸清了之后你会发现,很多「接 AI」的焦虑其实不是模型问题,而是老系统自身的历史包袱,先把这些包袱量化出来,改造才有抓手,这张清单就是后面每一步决策的起点。
一次只读审计
下面这段脚本只做一件事:连上库、扫一遍表和数据量、看看有没有测试目录,输出一份报告,不改任何数据。
# audit_legacy.py —— 只读审计,摸清老系统的底
import os
import sqlite3
from pathlib import Path
def audit_db(path):
conn = sqlite3.connect(path)
cur = conn.cursor()
tables = cur.execute(
"select name from sqlite_master where type='table' order by name"
).fetchall()
print("== 数据表 ==")
for (name,) in tables:
cnt = cur.execute(f'select count(*) from "{name}"').fetchone()[0]
print(f" {name}: {cnt} 行")
conn.close()
def audit_tests(root):
hits = list(Path(root).rglob("test_*.py"))
hits += list(Path(root).rglob("*_test.py"))
print(f"== 测试文件 == 找到 {len(hits)} 个")
for p in hits[:10]:
print(f" {p}")
if __name__ == "__main__":
root = os.environ.get("LEGACY_ROOT", ".")
audit_db(os.path.join(root, "legacy.db"))
audit_tests(root)
这里用 sqlite3 举例,换成 MySQL 或 Postgres 只是换一个驱动。跑一遍,你手里就有一张「现在系统长什么样」的清单:哪些表大、有没有测试、数据量多少。这就是接模型之前该做的准备——不是先写 prompt,而是先把底摸清。
下一章开始真正动手:从一个只读、可回滚的最小接入点切入,让模型先「看懂」一张表,而不是一上来就让它改数据。