1. 老 Java 系统接 AI,卡在哪一步
很多企业的核心业务系统是五到十年前用 Spring Boot + MyBatis 搭起来的,跑得稳、改得慢。现在业务方提需求:能不能让工单系统自动分类、让客服后台能问答、让报表支持自然语言查询。你第一反应可能是重构成 Python 技术栈,但算一下人力、时间和业务中断风险,基本就劝退了。
真正卡住的地方其实不是模型能力,而是三件事:一是老项目里没有统一的 AI 调用入口,每个业务模块各写各的 HTTP 请求,Key 散落在配置文件甚至硬编码里;二是 Java 工程师不熟悉大模型 SDK 的调用范式,流式返回、超时重试、异常处理都要重新踩坑;三是没法在不改核心业务代码的前提下,把 AI 能力像插件一样挂上去。
我试过的思路是:把 AI 调用收敛成一个独立的 service 层,用统一的 API 通道(TaoToken)屏蔽掉不同模型厂商的差异,老代码只依赖这个 service 的接口。这样核心业务逻辑一行不动,新增的 AI 能力通过依赖注入挂进来。下面这套配置骨架和验证流程,就是围绕这个思路展开的,适合 Spring Boot 2.x/3.x + MyBatis 的老项目,也适合想快速验证 AI 改造可行性的团队。
2. TaoToken 前置:统一 Key 与通道准备
TaoToken 在这里扮演的角色是「统一 API 通道」——你不需要在项目里分别对接多家模型厂商的 SDK,也不用为每个环境维护多套 Key。一个 Key、一个 base_url,就能调用不同模型。对老系统改造来说,这意味着一件事:AI 调用的配置项从 N 个变成 1 个,运维和权限管理都简单了。
你需要先拿到 API Key。进入控制台创建密钥,建议按环境(dev/staging/prod)分别建 Key,方便后续做额度隔离和问题定位。创建入口在控制台的 API Keys 页面,路径是console/api-keys。拿到 Key 之后先别急着写代码,把 base_url 记下来:https://taotoken.net/api,这个地址在后面的 application.yml 和 config.toml 里都会用到。
有一点要注意:Key 不要提交到 Git 仓库。老项目里常见的做法是写在 application.yml 里然后被一起提交,这个习惯要改。推荐用环境变量注入,或者用 Spring 的@ConfigurationProperties配合外部配置文件。下面给的骨架里我会用占位符标注,你替换成实际值即可。
如果你还想先确认模型对话效果再动手改代码,可以先用模型对话页面做一次手动验证,确认通道通、模型返回正常,再进入工程配置阶段。
3. 可复制配置:application.yml 与 config.toml 骨架
这一节是全文的核心,直接给可复制的配置。分两部分:Spring Boot 侧的 application.yml,以及如果你用 CLI 工具或本地 Agent 调试时的 config.toml。
3.1 依赖坐标
老项目大概率已经有 spring-boot-starter-web 和 lombok,这里只补 AI 调用需要的。如果你走 HTTP 直连方式(推荐,侵入最小),其实不需要额外的大模型 SDK 依赖,用 Spring 自带的 RestTemplate 或 WebClient 就够。但为了流式处理和 JSON 解析方便,建议加两个轻量依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>WebFlux 在这里只用来做流式响应,不影响你原有的 MVC 架构,两者可以共存。如果你的项目对依赖体积敏感,用 RestTemplate 也能跑通,只是流式处理要自己写回调。
3.2 application.yml 配置骨架
ai: taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY:sk-替换成你的Key} default-model: claude-sonnet-4-20250514 connect-timeout: 5000 read-timeout: 60000 max-retries: 2 stream: true spring: profiles: active: dev这里几个参数说明一下。connect-timeout设 5 秒,因为老系统所在的内网环境网络抖动可能比公网大,连接阶段不宜等太久。read-timeout给到 60 秒,是因为流式返回时首 token 可能来得慢,尤其是长 prompt 场景。max-retries设 2 次,配合幂等设计,避免网络抖动导致请求直接失败。stream: true是默认开启流式,如果你的业务场景是同步返回(比如后台批处理),可以按需关掉。
3.3 config.toml 配置骨架
如果你用 CLI 工具或本地 Agent 做调试,config.toml 的骨架如下:
[ai.taotoken] base_url = "https://taotoken.net/api" api_key = "sk-替换成你的Key" default_model = "claude-sonnet-4-20250514" connect_timeout = 5000 read_timeout = 60000 max_retries = 2 stream = true [ai.taotoken.models] chat = "claude-sonnet-4-20250514" code = "claude-sonnet-4-20250514"两个配置文件的字段是对齐的,方便你在本地调试和线上部署之间切换时不用改字段名。注意 config.toml 里的 api_key 同样不要提交到仓库,用.gitignore排除掉。
3.4 一个最小的 Service 封装
配置有了,接下来写一个 AiService,把调用逻辑收口。老项目里其他模块只依赖这个接口:
@Service public class AiService { @Value("${ai.taotoken.base-url}") private String baseUrl; @Value("${ai.taotoken.api-key}") private String apiKey; @Value("${ai.taotoken.default-model}") private String defaultModel; private final WebClient webClient; public AiService(WebClient.Builder builder) { this.webClient = builder.build(); } public String chat(String prompt) { Map<String, Object> body = new HashMap<>(); body.put("model", defaultModel); body.put("messages", List.of(Map.of("role", "user", "content", prompt))); body.put("stream", false); return webClient.post() .uri(baseUrl + "/v1/chat/completions") .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .bodyValue(body) .retrieve() .bodyToMono(String.class) .block(); } }这段代码的关键点是:base_url 和 Key 都从配置读,不硬编码;调用路径统一走/v1/chat/completions,这是兼容 OpenAI 格式的通用路径,TaoToken 的通道支持这个格式,所以换模型时业务代码不用动。老项目里原有的 Service 只需要注入 AiService,调chat()方法即可,核心业务逻辑零改动。
4. 验证请求:一次本地连通性检查
配置写完了,先别急着集成到业务里,做一次最小连通性验证。这一步的目的是确认 Key 有效、通道可达、模型返回正常。有三种方式,从简到繁。
4.1 curl 快速验证
最直接的方式是用 curl 打一次请求:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-替换成你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "用一句话说明什么是工单分类"}], "stream": false }'如果返回的 JSON 里有choices[0].message.content字段且内容正常,说明通道通了。如果返回 401,检查 Key 是否复制完整;返回 404,检查 base_url 是否多了或少了斜杠;返回超时,检查网络出口是否允许访问该域名。
4.2 单元测试验证
在项目里写一个简单的测试类,确认 Spring 上下文能正确加载配置:
@SpringBootTest class AiServiceTest { @Autowired private AiService aiService; @Test void testChat() { String result = aiService.chat("返回两个字:成功"); System.out.println("AI 返回:" + result); assertNotNull(result); } }跑通这个测试,说明 application.yml 的配置被正确读取、WebClient 能正常发起请求、返回结果能正确解析。这一步过了,再往业务模块里集成就稳了。
4.3 验证成功的判断标准
不要只看「有没有报错」,要看三个点:一是 HTTP 状态码是 200;二是返回体里 content 字段非空;三是响应时间在可接受范围内(同步调用建议 3 秒内,流式首 token 建议 2 秒内)。如果响应时间明显偏长,先排查是不是 read-timeout 设得太短导致重试,或者 prompt 太长导致模型处理慢。
5. 本篇常见错排查
这一节列几个我在老项目改造里实际踩过的坑,按出现频率排序。
第一个坑:Key 读不到,报 401。最常见的原因是环境变量没生效。Spring Boot 读取${TAOTOKEN_API_KEY}时,如果环境变量没设置,会 fallback 到冒号后面的默认值。如果你把默认值写成了真实 Key 又提交了仓库,等于泄露。建议默认值留空或写sk-please-set-env,强制走环境变量。
第二个坑:流式返回在 MVC 里被缓冲。老项目如果用了 Spring MVC 的@ResponseBody,流式返回可能被缓冲到完整响应才输出。解决办法是返回SseEmitter或Flux<String>,并确保produces = MediaType.TEXT_EVENT_STREAM_VALUE。如果业务不需要流式,直接把stream设为 false 最省事。
第三个坑:超时设置不合理导致重试风暴。如果 read-timeout 设成 10 秒,而模型处理长 prompt 需要 15 秒,就会触发超时重试,重试又超时,形成风暴。建议 read-timeout 不低于 60 秒,max-retries 不超过 2 次,并且重试要加退避。
第四个坑:模型名写错。不同模型的名称格式不一样,写错了会返回 404 或 model not found。建议把模型名也放到配置里,不要硬编码在代码中,换模型时只改配置。
第五个坑:老项目的 Jackson 版本冲突。如果老项目用的是较老的 Jackson 版本,解析返回体时可能报UnrecognizedPropertyException。解决办法是在 ObjectMapper 上关闭FAIL_ON_UNKNOWN_PROPERTIES,或者升级 Jackson 版本。
6. 下一步:从验证到长期编码
连通性验证通过之后,你就可以把 AiService 注入到具体的业务模块里了。比如工单系统里加一个自动分类的方法,客服后台加一个问答入口,报表模块加一个自然语言转 SQL 的辅助。每个模块只依赖 AiService 的接口,不直接碰 HTTP 调用,这样后续换模型、调参数、加缓存都在一处改。
如果你的团队打算把 AI 能力长期用在编码和 Agent 场景上,比如让 AI 辅助生成 MyBatis 的 Mapper、自动补全单元测试、或者做代码审查,那单次调用模式就不够用了,需要考虑 Coding Plan 这类面向长期编码场景的方案,额度和调用方式都更适合高频使用。
接入过程中如果遇到 Key 配置、超时、流式返回这类问题,可以先查接入文档,大部分报错都有对应的排查步骤。文档入口在doc路径下。需要管理多个环境的 Key 时,回到console/api-keys页面操作即可。整个改造的核心思路就一句话:配置收口、调用收口、业务不动,剩下的就是按模块逐步挂载 AI 能力。