1. Java 8 老系统接 AI,为什么不能直接给 API
生产环境还在跑 Java 8、Spring Boot 2.x 甚至更早,这是很多企业核心系统的真实状态。JDK 版本不够,Spring AI 这类新框架引不进来;依赖树牵一发动全身,升级一个包可能带崩一片;主流程流量压着,没人敢拿线上稳定性去赌一个 AI 试点。于是团队想接 AI,第一步就卡住了。
更危险的做法是硬塞。我见过在老系统 Service 层直接 new 一个 HttpClient 去调模型接口,Prompt 拼在业务代码里,超时没配、降级没有。模型响应一慢,订单查询的线程池被占满,主流程跟着卡。还有把用户手机号、身份证原样塞进 Prompt 的,过了两个月才被安全审计发现。
所以老系统接 AI 的核心不是"能不能调通模型",而是"AI 能不能安全地使用老系统能力"。这一篇聚焦一个具体问题:把既有 Java 8 的 API 包装成受控工具,用只读优先加权限拦截的方式,让 AI 能查、能总结、能建议,但不能直接改业务状态。适合正在推进遗留系统 AI 接入的后端和架构同学,跟着做能拿到一份可复制的 config.toml 骨架和权限拦截配置。
关键结论先放这里:AI 可以使用老系统能力,但不能直接拿到老系统 API。中间必须有一层工具中心,负责工具目录、权限策略、审计记录和返回值处理。下面从接入通道开始,一步步把这条链路搭起来。
2. TaoToken 前置:统一 Key 与 API 通道
老系统旁路接入 AI,最省事的做法是不要在业务代码里散落模型调用。所有模型请求走一个统一的 API 通道,Key 集中管理,这样审计、限流、替换模型都只改一处。TaoToken 在这里扮演的就是这个统一通道的角色,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。
你需要先拿到一个可用的 Key。登录后进入控制台,在 API Keys 页面创建一个新 Key,建议按环境区分命名,比如legacy-java8-lab,方便后面在审计日志里对应到具体系统。创建完成后把 Key 复制出来,只显示一次,丢了就重新建。
拿到 Key 之后,先别急着写 Java 代码。用一条 curl 确认通道是通的,这一步能排掉大部分"代码没问题但请求发不出去"的坑:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "只回复 ok"}], "max_tokens": 16 }'返回里能看到choices[0].message.content就说明通道正常。这一步过了,再进 Java 8 侧写工具包装。如果你还想先在网页上验证模型行为,可以直接用模型对话页面试几轮,确认模型对"只读查询"这类指令的理解符合预期,再去写代码。
3. 可复制配置:config.toml 骨架与权限拦截
Java 8 项目里不建议把模型地址、Key、权限规则硬编码。用一个config.toml把通道配置和工具权限策略分开,改策略不用重新编译。下面这份骨架可以直接抄,字段按你的环境替换。
# config.toml —— Java 8 老系统 AI 工具接入配置骨架 [gateway] # 统一 API 通道,所有模型请求走这里 base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读,不写死在文件里 model = "claude-sonnet-4-20250514" connect_timeout_ms = 3000 read_timeout_ms = 20000 max_retries = 1 [tool_center] # 工具目录:只有列在这里的能力才允许被 AI 调用 enabled_tools = ["queryOrder"] default_mode = "READ_ONLY" # 默认只读,写操作必须显式声明 [permission] # 写意图关键词,命中即拒绝,请求不会进入老系统 API write_intent_keywords = ["改成", "修改", "删除", "更新", "发货", "退款", "审批"] reject_reason = "WRITE_INTENT_REJECTED" allow_reason = "ALLOW_READ_ONLY_TOOL" [audit] enabled = true log_prefix = "[TOOL_AUDIT]" # 审计字段:租户、操作人、工具名、参数、模式 fields = ["tenant", "operator", "tool", "detail", "mode"] [mask] # 返回值脱敏规则,命中字段做掩码处理 masked_fields = ["customerMobile", "address", "idCard"] mobile_keep_prefix = 3 mobile_keep_suffix = 4配置里几个点值得单独说。api_key_env指向环境变量而不是明文,避免 Key 跟着代码进仓库。enabled_tools是白名单,没列进去的工具即使代码里写了也不会被 Agent 选中。write_intent_keywords是权限拦截的第一道闸,命中就直接返回拒绝,toolCalls为空,老系统 API 根本不会被触发。
对应的权限策略类可以这样写,逻辑简单但边界清楚:
public class ToolPermissionPolicy { private final List<String> writeKeywords; public ToolPermissionPolicy(List<String> writeKeywords) { this.writeKeywords = writeKeywords; } public boolean isWriteIntent(String question) { if (question == null) { return true; // 拿不准就按写处理,拒绝优先 } for (String kw : writeKeywords) { if (question.contains(kw)) { return true; } } return false; } }注意question == null时返回true,也就是默认拒绝。权限判断的原则是"拿不准就挡",而不是"拿不准就放"。这一条在真实项目里能挡掉很多边界情况。
4. 验证请求:只读优先的完整链路
配置就位后,跑一遍只读链路,确认从用户问题到脱敏返回整条路是通的。工具包装的核心是:Agent 调用的是 Tool,不是直接调用老系统 API。
public class OrderQueryTool { private final LegacyOrderApi legacyOrderApi; private final ToolAuditService auditService; private final ToolResultMasker masker; public OrderQueryTool(LegacyOrderApi legacyOrderApi, ToolAuditService auditService, ToolResultMasker masker) { this.legacyOrderApi = legacyOrderApi; this.auditService = auditService; this.masker = masker; } public ToolResult query(String tenantId, String operatorId, String orderId) { // 1. 审计:谁在什么租户下调用了什么工具 auditService.record(tenantId, operatorId, "queryOrder", "orderId=" + orderId, "READ_ONLY"); // 2. 调用老系统原始 API Order order = legacyOrderApi.queryOrder(orderId); // 3. 返回值脱敏后再交给模型 Order masked = masker.mask(order); return ToolResult.ok(masked); } }调用关系是:ToolCallingAgent选择queryOrder只读工具,进入OrderQueryTool.query,先记审计,再调LegacyOrderApi,最后过ToolResultMasker。这一层包装做了三件事:记录调用者、限定只读能力、脱敏返回值。
只读请求的输入和预期返回:
{ "answer": "订单 O202606050001 当前状态为 DELAYED,异常原因:仓库尚未确认出库时间。客户手机号和地址已脱敏。", "rejected": false, "rejectReason": "ALLOW_READ_ONLY_TOOL", "toolCalls": ["queryOrder"], "maskedFields": ["customerMobile", "address"] }观察三个点:rejected=false表示请求允许执行;toolCalls=["queryOrder"]表示只调了只读工具;maskedFields非空表示返回值做过脱敏。
再验证写操作拦截。输入"把 O202606050001 订单状态改成已发货",预期返回:
{ "answer": "该问题包含写操作意图,AI 不能直接修改老系统订单状态,请转人工审批。", "rejected": true, "rejectReason": "WRITE_INTENT_REJECTED", "toolCalls": [], "maskedFields": [] }关键是toolCalls=[]。请求在权限策略层就被挡住了,没有进入老系统 API。这比"调用之后发现不该改"安全得多,因为老系统的写接口从头到尾没被触碰。
脱敏效果对照一下更直观。老系统返回的原始值:
customerMobile=13812345678 address=上海市浦东新区某某路 88 号给模型和前端的结果:
customerMobile=138****5678 address=上海市浦东新区***工具调用的安全边界不只在调用前,也在调用后。入参决定 AI 能查什么,返回值决定 AI 能看什么,两边都要收口。
5. 本篇常见错排查
接入过程中踩过的坑集中在几处,对照排查能省不少时间。
请求发不出去,报连接超时。先确认base_url是https://taotoken.net/api,不要带多余路径。再用第 2 节的 curl 单独验证通道,curl 通了说明是 Java 侧配置问题,重点看connect_timeout_ms是不是设得太短,以及环境变量TAOTOKEN_API_KEY有没有真正注入到进程里。
写操作没被拦住,toolCalls不为空。检查write_intent_keywords是否覆盖了实际问法。关键词匹配是第一阶段方案,用户换个说法比如"帮我处理一下这个订单"可能绕过去。生产环境要升级成基于 RBAC 的动态权限,不同角色、不同租户能用的工具不同,关键词匹配远远不够。
返回值里还有敏感字段。检查masked_fields是否和实际字段名一致,大小写、驼峰命名都要对上。脱敏发生在ToolResultMasker,如果某个字段没进masked_fields,它会原样透传。建议在测试环境打印一次脱敏前后的对象做比对。
审计日志缺失。确认audit.enabled=true,并且ToolAuditService.record在调用老系统 API 之前执行。顺序反了的话,一旦老系统调用抛异常,审计就丢了,事后查不到这次调用发生过。
Agent 选错工具。检查enabled_tools白名单,没列进去的工具不会被选中。如果确实需要多个只读工具,逐个加进白名单,不要图省事放开全部。
模型响应慢拖垮老系统线程池。这是旁路接入要防的核心风险。read_timeout_ms设一个合理上限,配合max_retries=1,避免重试放大压力。更稳的做法是模型调用走独立线程池,和老系统业务线程池隔离,模型慢不影响主流程。
6. 从只读工具到企业级工具中心
这一篇把"API 不能直接交给 AI"落成了一条可运行的链路:老系统能力包装成工具,默认只读,调用前做权限判断,调用中记审计,调用后做脱敏,写操作转人工确认。跑通之后你会发现,工具中心的价值不是让 AI 多调几个接口,而是让 AI 使用系统能力时有清楚的边界。
从 Demo 到生产还差几步。动态权限模型要把关键词匹配换成基于角色的策略;工具注册中心要支持动态注册、版本管理和灰度发布,而不是硬编码;写操作的完整流程是 AI 生成建议、展示影响范围、人工确认、系统执行、审计记录,这条链路会在后续工作流章节展开;工具调用链路追踪要能回答哪个工具先调、哪个被拒、总耗时多少。
如果你正在推进企业 AI 工具接入落地,建议先把只读链路跑稳,再逐步放开能力。需要长期做编码和 Agent 集成的团队,可以了解 Coding Plan 把模型调用和工具编排统一管理;接入和排障过程中遇到通道问题,回到 API Keys 页面核对 Key 状态,再对照接入文档检查参数。把边界设计放在模型调用之前,老系统接 AI 这件事才走得稳。