:面试复盘!用 TaoToken 统一 Key 跑通 10 道高频题代码验证)
1. 面试背得滚瓜烂熟一跑代码就露馅Spring AI 面试题有个很尴尬的现象ChatClient、Advisor、RAG、Tool Calling 这些词你都能说上两句脑图也能画出来但面试官一句“你本地跑过吗Tool Calling 一次请求到底调了几次模型”就容易卡壳。我见过太多候选人把“Advisor 是拦截器链”背得很顺却说不清getOrder()顺序错了会出什么现象。这篇不打算再给你一份背诵版脑图而是把 10 道高频题变成 10 个最小可运行用例。核心思路是用 TaoToken 统一 Key 和 Base URL把模型接入这一层固定下来剩下的精力全花在验证每道题的行为上。这样你复习时不是“我记得”而是“我跑过结果是这个”。适合谁看正在准备 Java 后端 / AI 应用岗面试已经看过 Spring AI 基础概念但缺少动手验证的人。也适合想把脑图变成可复现工程目录的开发者。整篇围绕一个本地 Spring Boot 工程展开每题给一条 curl 或单测验证动作跑通即过关。先说清楚统一 Key 的价值。Spring AI 支持 OpenAI、DeepSeek、Ollama 等多种模型如果每换一个模型就改一套配置、换一个 Key验证 10 道题会变成配置地狱。TaoToken 提供 OpenAI 兼容的 Base URLSpring AI 的 OpenAI starter 可以直接指向它Key 只配一次模型 ID 按题切换。这样第 2 题验证结构化输出、第 4 题验证 Tool Calling底层接入完全不用动。我试过把 10 道题拆成 10 个独立的SpringBootTest每个测试只关注一个行为断言跑完一遍大概几分钟比反复读文档印象深得多。下面从工程搭建开始一步步来。2. TaoToken 前置统一 Key 与 Base URL 怎么配这一节解决“接入层固定”的问题。Spring AI 的 OpenAI starter 默认连 OpenAI 官方地址我们要做的是把base-url指向 TaoToken 的 API 地址api-key用 TaoToken 控制台生成的 Key模型 ID 按需填写。这样后面 10 道题共用同一套接入配置换题只换 model 或 prompt。先拿 Key。打开 TaoToken 控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite注册后在 API Keys 页面创建一个 Key复制保存。注意 Key 只在创建时完整显示一次丢了就重建一个。这个 Key 就是后面application.yml里spring.ai.openai.api-key的值。Base URL 用https://taotoken.net/api不要加多余路径。Spring AI 的 OpenAI 客户端会在 Base URL 后拼接/v1/chat/completions这类路径所以配置里只写到/api即可。如果你写成/api/v1实际请求会变成/api/v1/v1/chat/completions直接 404。模型 ID 这块TaoToken 支持多种模型你在模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite能看到可用列表。验证阶段建议选一个响应稳定的通用对话模型Tool Calling 题需要选支持 function calling 的模型否则模型不会返回tool_calls你会误以为是自己代码写错了。依赖方面Spring AI 1.0 GA 之后 starter 命名比较稳定。核心依赖是spring-ai-starter-model-openaiRAG 题额外需要spring-ai-starter-vector-store-pgvector或内存向量库Tool Calling 不需要额外依赖。MCP 题需要spring-ai-starter-mcp-client。版本统一用 Spring AI BOM 管理避免各 starter 版本漂移。这里有个容易踩的坑Spring AI 的 starter 名称在里程碑版本和 GA 版本之间改过。如果你搜到的教程写的是spring-ai-openai-spring-boot-starter那是旧命名GA 后是spring-ai-starter-model-openai。依赖拉不下来先检查命名。配置写完后先别急着写 10 道题用第 3 节的最小配置跑一个“你好”请求确认接入层通了再往下走。接入层不通后面所有题的报错都会长得像模型问题排查成本翻倍。3. 可复制配置application.yml 与依赖片段这一节给完整可复制的配置。先看pom.xml的关键片段用 BOM 统一版本dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies然后是application.yml这是统一 Key 的核心spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7注意api-key用环境变量注入不要把 Key 硬编码进仓库。本地跑测试时在 IDE 的运行配置里加TAOTOKEN_API_KEY你的Key或者用.env文件配合 spring-dotenv。这样 10 道题共用一份配置换模型只改model字段。如果你要验证多个模型可以定义多个ChatModelbean但更简单的做法是用ChatClient的options在单次调用时覆盖模型ChatResponse response chatClient.prompt() .user(用一句话解释什么是向量数据库) .options(OpenAiChatOptions.builder() .model(gpt-4o-mini) .build()) .call() .chatResponse();这样第 6 题验证向量库选型时可以临时切到另一个模型对比输出不用重启应用。RAG 题需要向量库配置。本地验证用内存向量库最省事加依赖spring-ai-starter-vector-store-simple配置里不用写连接信息。如果你要验证 PgVector配置如下spring: ai: vectorstore: pgvector: initialize-schema: true dimensions: 1536 index-type: HNSWdimensions必须和 Embedding 模型输出维度一致这是第 6 题的死穴。用text-embedding-3-small是 1536 维用别的模型先查清楚维度写错了插入向量时会报维度不匹配。Tool Calling 题不需要额外配置但要在Tool方法的 description 上花心思。MCP 题需要加spring: ai: mcp: client: enabled: true name: demo-client version: 1.0.0 type: SYNC配置齐了。下面进入逐题验证。每题我给一个最小用例和一条验证动作你可以按顺序跑也可以挑自己最虚的题先跑。4. 逐题验证从 ChatClient 到自动装配的 10 个最小用例这一节是主体10 道题按“接入 → 输入增强 → 输出控制 → 能力扩展 → 知识增强 → 工程化”的顺序排。每题先一句话点题再给代码或命令最后给验证动作。4.1 第 1 题ChatClient 与 ChatModel 的关系验证面试常问“ChatClient 和 ChatModel 什么关系”。口述是“门面与底层抽象”验证方式是注入两个 bean看 ChatClient 是否委托 ChatModel。SpringBootTest class ChatClientModelTest { Autowired private ChatClient chatClient; Autowired private ChatModel chatModel; Test void chatClientDelegatesToChatModel() { String viaClient chatClient.prompt() .user(回复client-ok) .call() .content(); String viaModel chatModel.call( new Prompt(回复model-ok)) .getResult().getOutput().getText(); assertThat(viaClient).isNotBlank(); assertThat(viaModel).isNotBlank(); } }验证动作跑这个测试两个断言都过说明 ChatClient 和 ChatModel 都能独立工作。再在chatClient调用处打断点能看到它内部持有ChatModel引用。面试时你可以说“我本地注入过两个 beanChatClient 的默认实现里组合了 ChatModel”。4.2 第 2 题PromptTemplate 与参数填充PromptTemplate 是输入增强的第一层。验证它是否真的把占位符替换掉。Test void promptTemplateFillsParams() { PromptTemplate template new PromptTemplate( 用一句话解释 {topic}面向 {audience}); Prompt prompt template.create(Map.of( topic, 向量检索, audience, Java 后端)); String content chatClient.prompt(prompt) .call().content(); assertThat(content).isNotBlank(); }验证动作把topic换成“RAG”观察输出是否围绕 RAG。如果输出里还出现{topic}字面量说明模板没生效检查是不是用了PromptTemplate.from()但没调create()。4.3 第 3 题ChatMemory 记忆是否真的带上历史记忆题的关键是验证第二轮请求里确实带了第一轮内容。用内存记忆最直观。Test void memoryKeepsHistory() { ChatClient client chatClient.mutate() .defaultAdvisors(new MessageChatMemoryAdvisor( new InMemoryChatMemory())) .build(); client.prompt().user(我叫阿强).call().content(); String second client.prompt() .user(我叫什么).call().content(); assertThat(second).contains(阿强); }验证动作跑测试第二轮回答里出现“阿强”即通过。如果没出现检查MessageChatMemoryAdvisor是否通过defaultAdvisors注册而不是单次advisors。这是第 8 题 Advisor 顺序的伏笔。4.4 第 4 题结构化输出转 Java 对象结构化输出验证的是“模型返回 JSONconverter 转对象”。定义一个 record用BeanOutputConverter。record Book(String title, String author, int year) {} Test void structuredOutputToBean() { BeanOutputConverterBook converter new BeanOutputConverter(Book.class); String raw chatClient.prompt() .user(u - u.text( 推荐一本 Java 书按以下格式输出 {format} ).param(format, converter.getFormat())) .call().content(); Book book converter.convert(raw); assertThat(book.title()).isNotBlank(); assertThat(book.year()).isGreaterThan(1900); }验证动作跑测试book字段都有值即通过。常见失败是模型返回带 markdown 代码块的 JSONconvert解析失败。可以在 prompt 里明确“只输出 JSON不要代码块”。4.5 第 5 题Tool Calling 到底调了几次模型这题面试加分项是“一次带工具的回答可能触发两次 ChatModel.call”。验证方式是定义工具观察调用日志。class WeatherTools { Tool(description 查询指定城市的天气) String getWeather(String city) { return city 晴25 度; } } Test void toolCallingInvokesTool() { String answer chatClient.prompt() .user(北京天气怎么样) .tools(new WeatherTools()) .call().content(); assertThat(answer).contains(25); }验证动作在getWeather里打日志跑测试能看到方法被调用一次。再在ChatModel调用处打日志能看到两次模型请求第一次返回tool_calls第二次带工具结果返回最终答案。如果工具没被调用把description写得更具体比如“查询中国城市实时天气输入城市名”。4.6 第 6 题RAG 链路与切分粒度RAG 验证分两步先确认文档能进向量库再确认检索能召回。用内存向量库和TokenTextSplitter。Test void ragRetrievesRelevantChunk() { VectorStore store new SimpleVectorStore(embeddingModel); ListDocument docs List.of( new Document(Spring AI 的 Advisor 是拦截器链), new Document(向量维度必须与 Embedding 模型一致)); TokenTextSplitter splitter new TokenTextSplitter(); store.add(splitter.apply(docs)); ListDocument hits store.similaritySearch( SearchRequest.builder() .query(Advisor 是什么) .topK(1).build()); assertThat(hits).isNotEmpty(); assertThat(hits.get(0).getText()).contains(Advisor); }验证动作跑测试召回的第一条包含“Advisor”即通过。把topK改成 2观察第二条是否相关。切分粒度的影响可以这样验证把长文档切成 100 token 和 500 token 两版同一问题看召回内容是否更聚焦。4.7 第 7 题向量库选型与维度绑定这题验证“换 Embedding 模型必须重建索引”。用两个不同维度的 Embedding 模型往同一个 PgVector 表里插观察报错。Test void dimensionMismatchFails() { // 假设表已按 1536 维建好 float[] wrongDim new float[768]; Arrays.fill(wrongDim, 0.1f); assertThatThrownBy(() - vectorStore.add(List.of( new Document(测试, Map.of(), wrongDim)))) .hasMessageContaining(dimension); }验证动作跑测试能看到维度不匹配的异常。这条验证让你在面试时能具体说出“维度绑定不是理论是插入时就报错”。选型结论已有 PG 用 PgVector已有 Redis 用 Redis千万级以上再考虑 Milvus。4.8 第 8 题Advisor 顺序错了会怎样这题验证“记忆 Advisor 必须在 RAG Advisor 之前”。构造两个 Advisor故意把顺序调反观察记忆失效。Test void advisorOrderMatters() { ChatClient client chatClient.mutate() .defaultAdvisors( new QuestionAnswerAdvisor(vectorStore), new MessageChatMemoryAdvisor(new InMemoryChatMemory()) ) .build(); client.prompt().user(我叫阿强).call().content(); String second client.prompt() .user(我叫什么).call().content(); // 顺序反了记忆可能失效 System.out.println(second second); }验证动作先跑反序版本再跑正序版本记忆在前对比第二轮回答是否包含“阿强”。正序通过、反序失败就证明了顺序的重要性。Advisor 的getOrder()默认值决定顺序自定义 Advisor 时显式实现getOrder()更稳。4.9 第 9 题多模态看图与文生图分开验证多模态分两半看图用UserMessage Media画图用ImageModel。看图验证Test void multimodalImageInput() { ClassPathResource image new ClassPathResource(test-image.png); String answer chatClient.prompt() .user(u - u.text(描述这张图) .media(MimeTypeUtils.IMAGE_PNG, image)) .call().content(); assertThat(answer).isNotBlank(); }验证动作准备一张本地图片跑测试输出应包含对图片内容的描述。注意模型必须支持多模态纯文本模型塞Media会报错。文生图单独用ImageModel验证返回的是临时 URL要持久化得自己下载落库。4.10 第 10 题自动装配与多 bean 冲突这题验证“换 starter 即换实现”和“多 starter 并存时 ChatClient 可能多 bean”。加两个模型 starter注入ChatClient观察是否报NoUniqueBeanDefinitionException。Test void multipleChatClientBeans() { // 同时引入 openai 和 ollama starter 时 // Autowired ChatClient 可能冲突 // 解决Primary 或按名称注入 assertThat(applicationContext .getBeansOfType(ChatClient.class)) .isNotEmpty(); }验证动作跑测试看ChatClientbean 数量。如果大于 1注入时用Qualifier或给主 bean 加Primary。自动装配的原理是 starter 里的AutoConfiguration.imports配合ConditionalOnClass类存在才装配。10 题跑完你对每个概念都有了一次“行为级”验证。下面把常见报错集中排一遍。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth跑这 10 道题时报错基本集中在接入层和模型能力两个方向。按报错原文对照排查比盲改配置快。401 UnauthorizedKey 没配或配错。检查TAOTOKEN_API_KEY环境变量是否真的注入到运行进程IDE 运行配置和终端export是两回事。另外确认 Key 没有多余空格复制时容易带上换行。如果 Key 正确仍 401去控制台确认 Key 状态是否正常。local proxy failed / connection refusedBase URL 写错或网络不通。确认base-url是https://taotoken.net/api不要带/v1。如果你本地配了系统级网络设置先确认它没有拦截这个域名。这个报错和 Key 无关别去重建 Key。reading choices / 解析响应失败通常是模型返回了非预期格式或者你用的模型 ID 不存在。检查model字段是否在 TaoToken 模型列表里。结构化输出题如果报这个多半是模型返回了 markdown 代码块包裹的 JSON在 prompt 里加“只输出 JSON”。OAuth / token 相关报错Spring AI 的 OpenAI starter 默认走 API Key 认证不会触发 OAuth。如果你看到 OAuth 字样检查是不是误引入了别的认证 starter或者api-key被当成了别的字段。Codex 的auth.json是另一套体系和 Spring AI 无关别混用。Tool Calling 不触发模型不支持 function calling或Tool的 description 太模糊。先换一个明确支持工具调用的模型再把 description 写成“动词 对象 输入说明”。验证时在工具方法里打日志没日志就是没触发。RAG 召回为空向量库没数据或维度不匹配或topK太小。先确认store.add()没抛异常再确认查询向量和入库向量用同一个 Embedding 模型。维度不一致时插入阶段就会报错不会等到查询。Advisor 没生效用了单次advisors()而不是defaultAdvisors()或者getOrder()返回顺序不对。全局生效用defaultAdvisors单次生效用advisors两者别混。顺序问题用第 8 题的正反序对比验证。多 bean 冲突同时引入多个模型 starter 时ChatClient和ChatModel都可能有多个 bean。给主 bean 加Primary或注入时用Qualifier(openAiChatClient)。自动装配题里这个现象本身就是考点。排障时记住一个原则先确认接入层Key Base URL 模型 ID三件套再怀疑业务代码。接入层不通所有题都会以奇怪的方式失败。6. 把脑图变成能跑的工程下一步怎么走10 道题跑完你手里应该有一个能跑的 Spring Boot 工程里面 10 个测试对应 10 个知识点。这比一张静态脑图有用得多因为每个节点都有对应的可执行验证。如果你想把验证做得更系统建议把 10 个测试收敛成一个spring-ai-interview-lab仓库每个测试类对应一道题README 里写清楚每题验证什么、预期结果是什么。面试前跑一遍比翻笔记快。接入层继续用 TaoToken 统一 Key 和 Base URL这样你换模型验证不同题时不用改配置。需要长期跑编码类 Agent 任务的话可以了解 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite把模型调用额度集中管理。验证模型输出行为用模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite快速试 prompt比每次改代码重启快。接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite里有 Base URL 和参数说明配置卡住时对照看。API Keys 管理在控制台https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteKey 轮换和额度查看都在那里。最后一个实用技巧把这 10 道题的验证动作写成 shell 脚本每次改完配置跑一遍几分钟出结果。面试前一周每天跑一次比临时背题踏实。工程能跑通面试时你说“我本地验证过”才有底气。