<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>LLM on XEDCZQ的博客</title><link>https://xedczq.cn/tags/llm/</link><description>Recent content in LLM on XEDCZQ的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Tue, 09 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://xedczq.cn/tags/llm/index.xml" rel="self" type="application/rss+xml"/><item><title>Ai面试项目：llm-provider模块</title><link>https://xedczq.cn/post/aiinterview_llmprovider/</link><pubDate>Tue, 09 Jun 2026 00:00:00 +0800</pubDate><guid>https://xedczq.cn/post/aiinterview_llmprovider/</guid><description>&lt;h2 id="llm-provider-模块设计与实现"&gt;&lt;a href="#llm-provider-%e6%a8%a1%e5%9d%97%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;Llm-provider 模块设计与实现
&lt;/h2&gt;&lt;p&gt;这篇笔记记录 &lt;code&gt;interview-guide&lt;/code&gt; 项目中 &lt;code&gt;llm-provider&lt;/code&gt; 模块的设计与接口实现。该模块负责统一管理大模型 Provider 配置，包括模型列表、默认模型、Embedding 能力、连通性测试，以及语音面试中 ASR/TTS 的运行时配置。&lt;/p&gt;
&lt;h2 id="模块能力概览"&gt;&lt;a href="#%e6%a8%a1%e5%9d%97%e8%83%bd%e5%8a%9b%e6%a6%82%e8%a7%88" class="header-anchor"&gt;&lt;/a&gt;模块能力概览
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Provider 管理：支持查询、创建、更新、删除大模型 Provider。&lt;/li&gt;
&lt;li&gt;双存储模式：兼容 DB 模式和 Legacy 配置文件模式。&lt;/li&gt;
&lt;li&gt;密钥保护：DB 模式下 API Key 使用 AES-GCM 加密存储，接口返回前统一脱敏。&lt;/li&gt;
&lt;li&gt;默认模型管理：区分默认 Chat Provider 和默认 Embedding Provider。&lt;/li&gt;
&lt;li&gt;缓存重载：Provider 变更后清空 &lt;code&gt;ChatClient&lt;/code&gt; 和 &lt;code&gt;EmbeddingModel&lt;/code&gt; 缓存，下次调用时重新构建。&lt;/li&gt;
&lt;li&gt;Embedding 校验：创建、更新和设置默认 Embedding Provider 时校验模型类型、维度和能力开关。&lt;/li&gt;
&lt;li&gt;连通性测试：支持对 LLM Provider 发起真实 HTTP 测试请求。&lt;/li&gt;
&lt;li&gt;语音配置管理：支持读取和更新 Qwen ASR/TTS 配置，并同步重载运行时服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="流程图"&gt;&lt;a href="#%e6%b5%81%e7%a8%8b%e5%9b%be" class="header-anchor"&gt;&lt;/a&gt;流程图
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;stateDiagram-v2
 [*] --&gt; ProviderConfigPage: 进入 LLM Provider 配置页

 ProviderConfigPage --&gt; ProviderListLoaded: GET /api/llm-provider/list
 ProviderListLoaded --&gt; ProviderDetailLoaded: GET /api/llm-provider/{id}

 ProviderListLoaded --&gt; CreatingProvider: POST /api/llm-provider
 CreatingProvider --&gt; ProviderSaved: 校验通过并保存
 CreatingProvider --&gt; Error: id重复 / 参数非法 / 写入失败

 ProviderDetailLoaded --&gt; UpdatingProvider: PUT /api/llm-provider/{id}
 UpdatingProvider --&gt; ProviderSaved: 局部字段更新成功
 UpdatingProvider --&gt; Error: Provider不存在 / 参数非法 / 写入失败

 ProviderSaved --&gt; RegistryReloaded: registry.reload()
 RegistryReloaded --&gt; ProviderListLoaded: 重新加载列表

 ProviderDetailLoaded --&gt; TestingProvider: POST /api/llm-provider/{id}/test
 TestingProvider --&gt; TestSuccess: 外部 LLM API 连接成功
 TestingProvider --&gt; TestFailed: 连接失败 / 鉴权失败 / 模型不可用
 TestSuccess --&gt; ProviderDetailLoaded
 TestFailed --&gt; ProviderDetailLoaded

 ProviderDetailLoaded --&gt; UpdatingDefaultChat: PUT /api/llm-provider/default-provider
 UpdatingDefaultChat --&gt; DefaultChatUpdated: Provider存在
 UpdatingDefaultChat --&gt; Error: Provider不存在 / defaultProvider为空

 ProviderDetailLoaded --&gt; UpdatingDefaultEmbedding: PUT /api/llm-provider/default-embedding-provider
 UpdatingDefaultEmbedding --&gt; DefaultEmbeddingUpdated: 支持 Embedding
 UpdatingDefaultEmbedding --&gt; Error: Provider不存在 / 不支持Embedding

 DefaultChatUpdated --&gt; RegistryReloaded
 DefaultEmbeddingUpdated --&gt; RegistryReloaded

 ProviderDetailLoaded --&gt; DeletingProvider: DELETE /api/llm-provider/{id}
 DeletingProvider --&gt; ProviderDeleted: 非默认Provider
 DeletingProvider --&gt; Error: Provider不存在 / 默认Provider不可删除
 ProviderDeleted --&gt; RegistryReloaded

 ProviderListLoaded --&gt; ManualReloading: POST /api/llm-provider/reload
 ManualReloading --&gt; RegistryReloaded

 ProviderConfigPage --&gt; VoiceConfigLoaded: GET /voice/asr 或 GET /voice/tts
 VoiceConfigLoaded --&gt; UpdatingVoiceConfig: PUT /voice/asr 或 PUT /voice/tts
 UpdatingVoiceConfig --&gt; VoiceConfigSaved: 写入YAML并reload ASR/TTS服务
 UpdatingVoiceConfig --&gt; Error: 配置写入失败

 VoiceConfigLoaded --&gt; TestingAsr: POST /voice/asr/test
 TestingAsr --&gt; AsrTestSuccess: WebSocket端口连接成功
 TestingAsr --&gt; AsrTestFailed: 连接失败
 AsrTestSuccess --&gt; VoiceConfigLoaded
 AsrTestFailed --&gt; VoiceConfigLoaded

 Error --&gt; ProviderConfigPage: 前端提示错误后返回配置页&lt;/pre&gt;&lt;h2 id="核心设计"&gt;&lt;a href="#%e6%a0%b8%e5%bf%83%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;核心设计
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;llm-provider&lt;/code&gt; 模块的核心是把“模型配置读取、密钥保护、默认模型选择、运行时客户端缓存”放在同一套服务中管理。&lt;/p&gt;
&lt;p&gt;DB 模式下，Provider 配置来自数据库。服务层读取 &lt;code&gt;LlmProviderEntity&lt;/code&gt; 后，会先解密 API Key，再做脱敏，然后转换为 &lt;code&gt;ProviderDTO&lt;/code&gt; 返回给前端。API Key 明文只在服务端运行时短暂出现，不会通过接口返回。&lt;/p&gt;
&lt;p&gt;Legacy 模式下，Provider 配置来自 &lt;code&gt;ConfigurationProperties&lt;/code&gt;。创建、更新和删除时会同步修改 YAML 配置文件和 &lt;code&gt;.env&lt;/code&gt; 文件，并在修改完成后重载 Provider 注册表。&lt;/p&gt;
&lt;p&gt;模块通过 &lt;code&gt;rwLock&lt;/code&gt; 控制并发读写：查询类接口使用读锁，创建、更新、删除和默认值修改使用写锁，避免配置在读写过程中出现不一致。&lt;/p&gt;
&lt;h2 id="provider-列表查询"&gt;&lt;a href="#provider-%e5%88%97%e8%a1%a8%e6%9f%a5%e8%af%a2" class="header-anchor"&gt;&lt;/a&gt;Provider 列表查询
&lt;/h2&gt;&lt;h3 id="get-apillm-providerlist-获取全部-provider-列表"&gt;&lt;a href="#get-apillm-providerlist-%e8%8e%b7%e5%8f%96%e5%85%a8%e9%83%a8-provider-%e5%88%97%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/llm-provider/list&lt;/code&gt; 获取全部 Provider 列表
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;List&amp;lt;ProviderDTO&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listProviders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listProviders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;globalSettingRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;1L&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;encryptionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Controller 调用 &lt;code&gt;listProviders()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Service 获取 &lt;code&gt;rwLock.readLock()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;DB 模式下先查询全局配置，用于判断默认 Chat Provider 和默认 Embedding Provider。&lt;/li&gt;
&lt;li&gt;查询全部 &lt;code&gt;LlmProviderEntity&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;遍历每个 Provider：
&lt;ul&gt;
&lt;li&gt;解密 API Key。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;maskApiKey(...)&lt;/code&gt; 脱敏。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;resolveEmbeddingDimensions(...)&lt;/code&gt; 解析向量维度，未配置时使用全局默认值。&lt;/li&gt;
&lt;li&gt;映射为 &lt;code&gt;ProviderDTO&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Legacy 模式下从 &lt;code&gt;properties.getProviders()&lt;/code&gt; 读取内存配置。&lt;/li&gt;
&lt;li&gt;返回 Provider 列表。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB 模式读取失败时会抛出 &lt;code&gt;BusinessException(PROVIDER_CONFIG_READ_FAILED)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;API Key 永远不会以明文返回给前端。&lt;/li&gt;
&lt;li&gt;当前存在一个问题：如果已启用 DB 存储 LLM 配置，更新配置文件和 API Key 后，即使重启项目也不会自动同步到 DB，除非关闭 DB 模式或清理数据库配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apillm-providerid-获取单个-provider-详情"&gt;&lt;a href="#get-apillm-providerid-%e8%8e%b7%e5%8f%96%e5%8d%95%e4%b8%aa-provider-%e8%af%a6%e6%83%85" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/llm-provider/{id}&lt;/code&gt; 获取单个 Provider 详情
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;ProviderDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Controller 接收 Provider &lt;code&gt;id&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Service 获取读锁。&lt;/li&gt;
&lt;li&gt;DB 模式下查询全局配置和目标 Provider。&lt;/li&gt;
&lt;li&gt;Provider 不存在时抛出 &lt;code&gt;BusinessException(PROVIDER_NOT_FOUND)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;解密 API Key 后脱敏。&lt;/li&gt;
&lt;li&gt;解析 Embedding 维度并构建 &lt;code&gt;ProviderDTO&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Legacy 模式下从内存配置中按 &lt;code&gt;id&lt;/code&gt; 获取 Provider。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="provider-创建与更新"&gt;&lt;a href="#provider-%e5%88%9b%e5%bb%ba%e4%b8%8e%e6%9b%b4%e6%96%b0" class="header-anchor"&gt;&lt;/a&gt;Provider 创建与更新
&lt;/h2&gt;&lt;h3 id="post-apillm-provider-创建新-provider"&gt;&lt;a href="#post-apillm-provider-%e5%88%9b%e5%bb%ba%e6%96%b0-provider" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/llm-provider&lt;/code&gt; 创建新 Provider
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;existsById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;validateEmbeddingConfig&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;encryptionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Controller 接收 &lt;code&gt;CreateProviderRequest&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;@Valid&lt;/code&gt; 校验 &lt;code&gt;id&lt;/code&gt;、&lt;code&gt;baseUrl&lt;/code&gt;、&lt;code&gt;apiKey&lt;/code&gt;、&lt;code&gt;model&lt;/code&gt; 均不能为空。&lt;/li&gt;
&lt;li&gt;Service 开启事务并获取写锁。&lt;/li&gt;
&lt;li&gt;DB 模式下先检查 Provider ID 是否已存在。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;baseUrl&lt;/code&gt;、&lt;code&gt;model&lt;/code&gt;、&lt;code&gt;apiKey&lt;/code&gt; 做二次非空校验。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;validateEmbeddingConfig(...)&lt;/code&gt; 校验 Embedding 配置。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;encryptionService.encrypt(apiKey)&lt;/code&gt; 加密 API Key。&lt;/li&gt;
&lt;li&gt;保存 &lt;code&gt;LlmProviderEntity&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;registry.reload()&lt;/code&gt; 清空运行时缓存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Legacy 模式处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查 &lt;code&gt;properties.getProviders()&lt;/code&gt; 中是否已有相同 ID。&lt;/li&gt;
&lt;li&gt;构建 &lt;code&gt;ProviderConfig&lt;/code&gt; 并写入内存 Map。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;writeProviderToYaml(...)&lt;/code&gt; 写回 YAML。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;writeEnvValue(...)&lt;/code&gt; 写入 &lt;code&gt;.env&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;registry.reload()&lt;/code&gt; 重载缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Embedding 校验逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;supportsEmbedding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 抛出错误&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;looksLikeChatModel&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 抛出错误并给出推荐&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;embeddingDimensions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 抛出错误&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="put-apillm-providerid-更新-provider"&gt;&lt;a href="#put-apillm-providerid-%e6%9b%b4%e6%96%b0-provider" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/llm-provider/{id}&lt;/code&gt; 更新 Provider
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;validateEmbeddingConfig&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;encryptionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newApiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;providerRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Controller 接收 Provider &lt;code&gt;id&lt;/code&gt; 和 &lt;code&gt;UpdateProviderRequest&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Service 开启事务并获取写锁。&lt;/li&gt;
&lt;li&gt;DB 模式下根据 &lt;code&gt;id&lt;/code&gt; 查询 Provider。&lt;/li&gt;
&lt;li&gt;Provider 不存在时抛出 &lt;code&gt;BusinessException(PROVIDER_NOT_FOUND)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;按字段更新配置：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;baseUrl&lt;/code&gt;：&lt;code&gt;null&lt;/code&gt; 表示不修改，空字符串非法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt;：&lt;code&gt;null&lt;/code&gt; 表示不修改，空字符串非法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apiKey&lt;/code&gt;：&lt;code&gt;null&lt;/code&gt; 表示不修改，空字符串非法，更新时重新加密。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;embeddingModel&lt;/code&gt;：允许传 &lt;code&gt;null&lt;/code&gt; 清除。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;embeddingDimensions&lt;/code&gt;：按请求值更新。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;supportsEmbedding&lt;/code&gt;：按请求值更新。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;temperature&lt;/code&gt;：按请求值更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;validateEmbeddingConfig(...)&lt;/code&gt; 做完整校验。&lt;/li&gt;
&lt;li&gt;保存实体并重载缓存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UpdateProviderRequest&lt;/code&gt; 没有 &lt;code&gt;@Valid&lt;/code&gt;，所有字段都是可选字段。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt; 表示不更新。&lt;/li&gt;
&lt;li&gt;空字符串视为非法输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="provider-删除与重载"&gt;&lt;a href="#provider-%e5%88%a0%e9%99%a4%e4%b8%8e%e9%87%8d%e8%bd%bd" class="header-anchor"&gt;&lt;/a&gt;Provider 删除与重载
&lt;/h2&gt;&lt;h3 id="delete-apillm-providerid-删除-provider"&gt;&lt;a href="#delete-apillm-providerid-%e5%88%a0%e9%99%a4-provider" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/llm-provider/{id}&lt;/code&gt; 删除 Provider
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 开启事务并获取写锁。&lt;/li&gt;
&lt;li&gt;DB 模式下读取全局设置。&lt;/li&gt;
&lt;li&gt;判断当前 Provider 是否为默认 Chat Provider 或默认 Embedding Provider。&lt;/li&gt;
&lt;li&gt;如果是默认 Provider，抛出 &lt;code&gt;BusinessException(PROVIDER_DEFAULT_CANNOT_DELETE)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;查询目标 Provider，确认存在后删除。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;registry.reload()&lt;/code&gt; 清空运行时缓存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Legacy 模式处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查是否为默认 Provider。&lt;/li&gt;
&lt;li&gt;从内存 Map 中删除配置。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;removeProviderFromYaml(...)&lt;/code&gt; 删除 YAML 节点。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;removeFromEnv(...)&lt;/code&gt; 删除 &lt;code&gt;.env&lt;/code&gt; 中的 API Key。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;registry.reload()&lt;/code&gt; 重载缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保护机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认 Chat Provider 和默认 Embedding Provider 不允许直接删除。&lt;/li&gt;
&lt;li&gt;必须先切换默认值，再删除原 Provider。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apillm-providerreload-手动重载-provider-缓存"&gt;&lt;a href="#post-apillm-providerreload-%e6%89%8b%e5%8a%a8%e9%87%8d%e8%bd%bd-provider-%e7%bc%93%e5%ad%98" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/llm-provider/reload&lt;/code&gt; 手动重载 Provider 缓存
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;clientCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;embeddingModelCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该接口不加锁。&lt;/li&gt;
&lt;li&gt;不开启事务。&lt;/li&gt;
&lt;li&gt;不访问 DB。&lt;/li&gt;
&lt;li&gt;只清空内存中的 &lt;code&gt;ChatClient&lt;/code&gt; 和 &lt;code&gt;EmbeddingModel&lt;/code&gt; 缓存。&lt;/li&gt;
&lt;li&gt;下次调用 &lt;code&gt;getChatClient()&lt;/code&gt; 或获取 Embedding 模型时按最新配置重新构建。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="provider-连通性测试"&gt;&lt;a href="#provider-%e8%bf%9e%e9%80%9a%e6%80%a7%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;Provider 连通性测试
&lt;/h2&gt;&lt;h3 id="post-apillm-provideridtest-测试-provider-连接"&gt;&lt;a href="#post-apillm-provideridtest-%e6%b5%8b%e8%af%95-provider-%e8%bf%9e%e6%8e%a5" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/llm-provider/{id}/test&lt;/code&gt; 测试 Provider 连接
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;ProviderTestResult&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 获取读锁。&lt;/li&gt;
&lt;li&gt;根据模式读取运行时配置：
&lt;ul&gt;
&lt;li&gt;DB 模式下调用 &lt;code&gt;getProviderRuntimeConfigOrThrow(id)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Legacy 模式下调用 &lt;code&gt;toRuntimeConfig(...)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;构建 &lt;code&gt;RestClient&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;connectTimeout = 5s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readTimeout = 10s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Header 中设置 &lt;code&gt;Authorization: Bearer {apiKey}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;构建测试请求体：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;xxx&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;messages&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Reply with OK only.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;max_tokens&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="5"&gt;
&lt;li&gt;构建候选测试 URL：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;baseUrl + &amp;quot;/chat/completions&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;baseUrl&lt;/code&gt; 不含版本号，再尝试 &lt;code&gt;baseUrl + &amp;quot;/v1/chat/completions&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;依次向候选 URL 发送 POST 请求。&lt;/li&gt;
&lt;li&gt;任一 URL 成功时返回连接成功。&lt;/li&gt;
&lt;li&gt;全部失败时返回最后一次失败原因。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是 Provider 管理中唯一会直接调用外部 LLM API 的接口。&lt;/li&gt;
&lt;li&gt;测试请求会发送真实 HTTP 请求。&lt;/li&gt;
&lt;li&gt;HTTP 错误会记录状态码和响应体，普通异常会记录异常类型和错误信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="默认-provider-管理"&gt;&lt;a href="#%e9%bb%98%e8%ae%a4-provider-%e7%ae%a1%e7%90%86" class="header-anchor"&gt;&lt;/a&gt;默认 Provider 管理
&lt;/h2&gt;&lt;h3 id="get-apillm-providerdefault-provider-获取默认-provider"&gt;&lt;a href="#get-apillm-providerdefault-provider-%e8%8e%b7%e5%8f%96%e9%bb%98%e8%ae%a4-provider" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/llm-provider/default-provider&lt;/code&gt; 获取默认 Provider
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;DefaultProviderDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 获取读锁。&lt;/li&gt;
&lt;li&gt;DB 模式下查询 &lt;code&gt;globalSettingRepository.findById(1L)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;返回默认 Chat Provider ID 和默认 Embedding Provider ID。&lt;/li&gt;
&lt;li&gt;Legacy 模式下从 &lt;code&gt;properties.defaultProvider&lt;/code&gt; 和 &lt;code&gt;properties.defaultEmbeddingProvider&lt;/code&gt; 构建返回值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;返回结构：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultProvider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;dashscope&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultEmbeddingProvider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;dashscope&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="put-apillm-providerdefault-provider-设置默认-chat-provider"&gt;&lt;a href="#put-apillm-providerdefault-provider-%e8%ae%be%e7%bd%ae%e9%bb%98%e8%ae%a4-chat-provider" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/llm-provider/default-provider&lt;/code&gt; 设置默认 Chat Provider
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 开启事务并获取写锁。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;request.defaultProvider()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;默认 Provider 为空时抛出 &lt;code&gt;BAD_REQUEST&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;查询目标 Provider，确认存在。&lt;/li&gt;
&lt;li&gt;DB 模式下更新 &lt;code&gt;GlobalSettingEntity.defaultChatProviderId&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;保存全局设置。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;registry.reload()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Legacy 模式处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;校验 Provider 存在。&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;properties.setDefaultProvider(providerId)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;writeDefaultProviderToYaml(providerId)&lt;/code&gt; 写回配置。&lt;/li&gt;
&lt;li&gt;删除旧的 &lt;code&gt;module-defaults&lt;/code&gt; 配置。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;registry.reload()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apillm-providerdefault-embedding-provider-设置默认-embedding-provider"&gt;&lt;a href="#put-apillm-providerdefault-embedding-provider-%e8%ae%be%e7%bd%ae%e9%bb%98%e8%ae%a4-embedding-provider" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/llm-provider/default-embedding-provider&lt;/code&gt; 设置默认 Embedding Provider
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 开启事务并获取写锁。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;request.defaultEmbeddingProvider()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;默认 Embedding Provider 为空时抛出 &lt;code&gt;BAD_REQUEST&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;查询目标 Provider，确认存在。&lt;/li&gt;
&lt;li&gt;校验该 Provider 支持 Embedding：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;supportsEmbedding&lt;/code&gt; 必须为 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;embeddingModel&lt;/code&gt; 必须存在。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validateEmbeddingConfig(...)&lt;/code&gt; 必须通过。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DB 模式下更新 &lt;code&gt;GlobalSettingEntity.defaultEmbeddingProviderId&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;保存全局设置。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;registry.reload()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;与默认 Chat Provider 的差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置默认 Embedding Provider 时多了 Embedding 能力校验。&lt;/li&gt;
&lt;li&gt;不支持 Embedding 的 Provider 不能被设置为默认向量服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="asr-配置管理"&gt;&lt;a href="#asr-%e9%85%8d%e7%bd%ae%e7%ae%a1%e7%90%86" class="header-anchor"&gt;&lt;/a&gt;ASR 配置管理
&lt;/h2&gt;&lt;h3 id="get-apillm-providervoiceasr-获取-asr-配置"&gt;&lt;a href="#get-apillm-providervoiceasr-%e8%8e%b7%e5%8f%96-asr-%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/llm-provider/voice/asr&lt;/code&gt; 获取 ASR 配置
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;AsrConfigDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 获取读锁。&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;VoiceInterviewProperties&lt;/code&gt; 读取 &lt;code&gt;voiceProperties.getQwen().getAsr()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;构建 &lt;code&gt;AsrConfigDTO&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;format&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sampleRate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maskedApiKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enableTurnDetection&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnDetectionType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnDetectionThreshold&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnDetectionSilenceDurationMs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VAD 相关参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;返回脱敏后的 ASR 配置。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ASR 配置来源于 &lt;code&gt;VoiceInterviewProperties&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;配置前缀是 &lt;code&gt;app.voice-interview&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;该配置不走 DB。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apillm-providervoiceasr-更新-asr-配置"&gt;&lt;a href="#put-apillm-providervoiceasr-%e6%9b%b4%e6%96%b0-asr-%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/llm-provider/voice/asr&lt;/code&gt; 更新 ASR 配置
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 获取写锁。&lt;/li&gt;
&lt;li&gt;读取运行时 ASR 和 TTS 配置引用。&lt;/li&gt;
&lt;li&gt;按字段更新 ASR 配置：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;format&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sampleRate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enableTurnDetection&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnDetectionType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnDetectionThreshold&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnDetectionSilenceDurationMs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果更新了 API Key，则同步更新 ASR 和 TTS：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;updateEnvValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;AI_BAILIAN_API_KEY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="5"&gt;
&lt;li&gt;调用 &lt;code&gt;writeAsrConfigToYaml(asr)&lt;/code&gt; 写回 YAML。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;asrService.reload(voiceProperties)&lt;/code&gt; 重载 ASR 服务。&lt;/li&gt;
&lt;li&gt;如果 API Key 更新，则同步调用 &lt;code&gt;ttsService.reload(voiceProperties)&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该方法没有 &lt;code&gt;@Transactional&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;ASR 和 TTS 共享百炼 API Key。&lt;/li&gt;
&lt;li&gt;修改 ASR 的 API Key 会同步影响 TTS。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="tts-配置管理"&gt;&lt;a href="#tts-%e9%85%8d%e7%bd%ae%e7%ae%a1%e7%90%86" class="header-anchor"&gt;&lt;/a&gt;TTS 配置管理
&lt;/h2&gt;&lt;h3 id="get-apillm-providervoicetts-获取-tts-配置"&gt;&lt;a href="#get-apillm-providervoicetts-%e8%8e%b7%e5%8f%96-tts-%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/llm-provider/voice/tts&lt;/code&gt; 获取 TTS 配置
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;TtsConfigDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 获取读锁。&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;VoiceInterviewProperties&lt;/code&gt; 读取 &lt;code&gt;voiceProperties.getQwen().getTts()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;构建 &lt;code&gt;TtsConfigDTO&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maskedApiKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;voice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;format&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sampleRate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languageType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;speechRate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volume&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;返回脱敏后的 TTS 配置。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="put-apillm-providervoicetts-更新-tts-配置"&gt;&lt;a href="#put-apillm-providervoicetts-%e6%9b%b4%e6%96%b0-tts-%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/llm-provider/voice/tts&lt;/code&gt; 更新 TTS 配置
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 获取写锁。&lt;/li&gt;
&lt;li&gt;读取运行时 ASR 和 TTS 配置引用。&lt;/li&gt;
&lt;li&gt;按字段更新 TTS 配置：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;voice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;format&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sampleRate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languageType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;speechRate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volume&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果更新了 API Key，则同步更新 TTS 和 ASR：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;updateEnvValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;AI_BAILIAN_API_KEY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="5"&gt;
&lt;li&gt;调用 &lt;code&gt;writeTtsConfigToYaml(tts)&lt;/code&gt; 写回 YAML。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;ttsService.reload(voiceProperties)&lt;/code&gt; 重载 TTS 服务。&lt;/li&gt;
&lt;li&gt;如果 API Key 更新，则同步调用 &lt;code&gt;asrService.reload(voiceProperties)&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TTS 更新逻辑与 ASR 对称。&lt;/li&gt;
&lt;li&gt;ASR/TTS 的 API Key 始终联动更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="asr-连通性测试"&gt;&lt;a href="#asr-%e8%bf%9e%e9%80%9a%e6%80%a7%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;ASR 连通性测试
&lt;/h2&gt;&lt;h3 id="post-apillm-providervoiceasrtest-测试-asr-连接"&gt;&lt;a href="#post-apillm-providervoiceasrtest-%e6%b5%8b%e8%af%95-asr-%e8%bf%9e%e6%8e%a5" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/llm-provider/voice/asr/test&lt;/code&gt; 测试 ASR 连接
&lt;/h3&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;ProviderTestResult&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Service 获取读锁。&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;voiceProperties.getQwen().getAsr()&lt;/code&gt; 读取 ASR 配置。&lt;/li&gt;
&lt;li&gt;解析 WebSocket URL：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wss&lt;/code&gt; 默认端口为 &lt;code&gt;443&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ws&lt;/code&gt; 默认端口为 &lt;code&gt;80&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;使用 TCP Socket 发起连接测试：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="5"&gt;
&lt;li&gt;连接成功时返回：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ProviderTestResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ASR WebSocket 连接成功: host&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="6"&gt;
&lt;li&gt;连接失败时返回失败原因。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;与 Provider 连通性测试的差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ASR 测试只做 TCP Socket 连接。&lt;/li&gt;
&lt;li&gt;不发送 WebSocket 握手。&lt;/li&gt;
&lt;li&gt;不调用真实 ASR 识别接口。&lt;/li&gt;
&lt;li&gt;Provider 测试会发送真实 HTTP 请求到 LLM 服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="缓存与运行时行为"&gt;&lt;a href="#%e7%bc%93%e5%ad%98%e4%b8%8e%e8%bf%90%e8%a1%8c%e6%97%b6%e8%a1%8c%e4%b8%ba" class="header-anchor"&gt;&lt;/a&gt;缓存与运行时行为
&lt;/h2&gt;&lt;p&gt;Provider 配置变更后都会调用 &lt;code&gt;registry.reload()&lt;/code&gt;。这个方法会清空内部缓存：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;clientCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;embeddingModelCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此，配置变更不会立即创建新的客户端，而是在下一次业务代码调用 Provider 时按需重建。这种方式避免了更新接口直接承担模型客户端初始化成本，也能保证旧配置不会长期停留在缓存中。&lt;/p&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;reload&lt;/code&gt; 只负责清空缓存，不负责同步配置源。如果 DB 模式已经启用，系统会优先读取数据库配置，而不是重新从 YAML 或 &lt;code&gt;.env&lt;/code&gt; 导入配置。&lt;/p&gt;
&lt;h2 id="当前问题与优化方向"&gt;&lt;a href="#%e5%bd%93%e5%89%8d%e9%97%ae%e9%a2%98%e4%b8%8e%e4%bc%98%e5%8c%96%e6%96%b9%e5%90%91" class="header-anchor"&gt;&lt;/a&gt;当前问题与优化方向
&lt;/h2&gt;&lt;p&gt;当前模块已经支持 DB 模式和 Legacy 模式，但配置同步边界还需要进一步明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB 模式启用后，YAML 和 &lt;code&gt;.env&lt;/code&gt; 的修改不会自动回写数据库。&lt;/li&gt;
&lt;li&gt;重启项目只能重新加载运行时配置，不能解决 DB 配置与文件配置不一致的问题。&lt;/li&gt;
&lt;li&gt;手动 &lt;code&gt;reload&lt;/code&gt; 只清空运行时缓存，不会重新导入配置源。&lt;/li&gt;
&lt;li&gt;ASR/TTS 配置仍来自 &lt;code&gt;VoiceInterviewProperties&lt;/code&gt;，与 Provider DB 配置不是同一套存储。&lt;/li&gt;
&lt;li&gt;ASR/TTS 更新方法没有事务，写 YAML、写 &lt;code&gt;.env&lt;/code&gt;、服务重载之间存在部分成功的可能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续可按以下方向优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;增加 DB 模式下的配置导入接口，用于从 YAML 和 &lt;code&gt;.env&lt;/code&gt; 同步 Provider 到数据库。&lt;/li&gt;
&lt;li&gt;在启动阶段增加一次性迁移策略，明确 DB 优先还是配置文件优先。&lt;/li&gt;
&lt;li&gt;给 Provider 配置增加版本号或更新时间，便于排查缓存是否已刷新。&lt;/li&gt;
&lt;li&gt;将 ASR/TTS 配置纳入统一配置存储，减少双配置源带来的不一致。&lt;/li&gt;
&lt;li&gt;对 YAML 写入、&lt;code&gt;.env&lt;/code&gt; 写入和服务重载增加失败补偿或更明确的错误提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;llm-provider&lt;/code&gt; 模块承担了大模型能力的统一配置入口。它不仅管理 Chat Provider，还管理 Embedding Provider、默认模型、运行时缓存和语音 ASR/TTS 配置。模块的关键价值在于：把模型配置和业务调用解耦，让知识库、RAG 聊天、语音面试等上层能力都可以通过统一 Provider 注册表获取模型能力。后续重点是进一步梳理 DB 配置和文件配置的同步机制，让配置来源更清晰、运行时状态更可控。&lt;/p&gt;</description></item><item><title>Transformer 20 步可视化学习笔记</title><link>https://xedczq.cn/post/transformerexplainer/</link><pubDate>Fri, 05 Jun 2026 23:10:00 +0800</pubDate><guid>https://xedczq.cn/post/transformerexplainer/</guid><description>&lt;img src="https://xedczq.cn/img/transformer-explainer/steps/step-03.jpg" alt="Featured image of post Transformer 20 步可视化学习笔记" /&gt;&lt;h1 id="transformer-20-步可视化学习笔记"&gt;&lt;a href="#transformer-20-%e6%ad%a5%e5%8f%af%e8%a7%86%e5%8c%96%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0" class="header-anchor"&gt;&lt;/a&gt;Transformer 20 步可视化学习笔记
&lt;/h1&gt;&lt;p&gt;本文参考 &lt;a class="link" href="https://poloclub.github.io/transformer-explainer/" target="_blank" rel="noopener"
 &gt;Transformer Explainer&lt;/a&gt; 的交互式讲解，按它的 20 个步骤整理一篇中文学习笔记。这个网站用 GPT-2 small 作为示例模型，把文本生成过程可视化成从输入 token 到输出概率的完整流水线。&lt;/p&gt;
&lt;p&gt;先记住一句话：&lt;strong&gt;GPT 类 Transformer 的核心任务是下一个 token 预测&lt;/strong&gt;。给定提示词：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Data visualization empowers users to
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模型要回答的问题是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;下一个最可能出现的 token 是什么？
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为了回答这个问题，Transformer 会经历：分词、嵌入、位置编码、多层 Transformer Block、自注意力、MLP、logits、概率分布、采样策略等步骤。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;本文所有步骤截图均截取自 &lt;a class="link" href="https://poloclub.github.io/transformer-explainer/" target="_blank" rel="noopener"
 &gt;Transformer Explainer&lt;/a&gt;，该项目由 Georgia Tech Polo Club 团队开发。截图用于个人学习笔记，建议结合原网站交互查看。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="what-is-transformertransformer-是什么"&gt;&lt;a href="#what-is-transformertransformer-%e6%98%af%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;What is Transformer：Transformer 是什么
&lt;/h2&gt;&lt;p&gt;Transformer 是现代大语言模型最常用的基础架构。GPT、Llama、Gemini 这类文本生成模型，核心都可以理解为 Transformer 架构的扩展版本。&lt;/p&gt;
&lt;p&gt;它最重要的能力不是“背答案”，而是从大量文本中学会语言模式，然后在推理时根据上下文预测下一个 token。这个预测会反复进行：预测一个 token，把它接到原文本后面，再继续预测下一个。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 1: What is Transformer" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-01.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer，&lt;a class="link" href="https://poloclub.github.io/transformer-explainer/" target="_blank" rel="noopener"
 &gt;https://poloclub.github.io/transformer-explainer/&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="how-transformers-work文本生成的本质"&gt;&lt;a href="#how-transformers-work%e6%96%87%e6%9c%ac%e7%94%9f%e6%88%90%e7%9a%84%e6%9c%ac%e8%b4%a8" class="header-anchor"&gt;&lt;/a&gt;How Transformers Work：文本生成的本质
&lt;/h2&gt;&lt;p&gt;Transformer 生成文本时并不是一次性写完整段话，而是逐步生成。每一步都在做同一个任务：&lt;strong&gt;根据已有上下文，预测下一个 token 的概率分布&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;例如当前输入是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Data visualization empowers users to
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模型可能认为下一个 token 是 &lt;code&gt;visualize&lt;/code&gt; 的概率最高，也可能给 &lt;code&gt;create&lt;/code&gt;、&lt;code&gt;see&lt;/code&gt;、&lt;code&gt;make&lt;/code&gt; 等 token 分配较高概率。最终输出哪个 token，还会受到 temperature、top-k、top-p 等采样参数影响。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 2: How Transformers Work" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-02.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="transformer-architecture整体架构"&gt;&lt;a href="#transformer-architecture%e6%95%b4%e4%bd%93%e6%9e%b6%e6%9e%84" class="header-anchor"&gt;&lt;/a&gt;Transformer Architecture：整体架构
&lt;/h2&gt;&lt;p&gt;一个文本生成 Transformer 可以拆成三大部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embedding&lt;/strong&gt;：把人类文本变成模型能处理的向量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transformer Blocks&lt;/strong&gt;：反复加工每个 token 的表示，核心包括 Self-Attention 和 MLP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output Probabilities&lt;/strong&gt;：把最终向量变成词表中每个 token 的概率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从宏观上看，信息流是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;文本输入 -&amp;gt; token -&amp;gt; embedding -&amp;gt; 多层 Transformer Block -&amp;gt; logits -&amp;gt; 概率 -&amp;gt; 采样下一个 token
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img alt="Step 3: Transformer Architecture" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-03.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="embedding把文本变成向量"&gt;&lt;a href="#embedding%e6%8a%8a%e6%96%87%e6%9c%ac%e5%8f%98%e6%88%90%e5%90%91%e9%87%8f" class="header-anchor"&gt;&lt;/a&gt;Embedding：把文本变成向量
&lt;/h2&gt;&lt;p&gt;模型不能直接理解字符串。Embedding 的作用是把每个 token 转成一串数字，也就是向量。这个向量不是随便编码的，而是在训练过程中学出来的。&lt;/p&gt;
&lt;p&gt;如果两个 token 经常出现在相似语境中，它们的 embedding 往往会在高维空间中更接近。可以把 embedding 理解成模型内部的“词义坐标”。&lt;/p&gt;
&lt;p&gt;GPT-2 small 的隐藏维度是 768，所以每个 token 会被表示成一个 768 维向量。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 4: Embedding" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-04.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="token-embedding分词与-token-查表"&gt;&lt;a href="#token-embedding%e5%88%86%e8%af%8d%e4%b8%8e-token-%e6%9f%a5%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;Token Embedding：分词与 token 查表
&lt;/h2&gt;&lt;p&gt;Tokenization 会把输入文本切成 token。token 可以是完整单词，也可以是子词。例如示例里的 &lt;code&gt;empowers&lt;/code&gt; 被切成了 &lt;code&gt;em&lt;/code&gt; 和 &lt;code&gt;powers&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;每个 token 都有一个唯一 ID。GPT-2 的词表大小是 50,257，所以 token embedding 矩阵大致是：&lt;/p&gt;
$$
50257 \times 768
$$&lt;p&gt;模型拿到 token ID 后，会去这个大矩阵里查出对应的 768 维向量。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 5: Token Embedding" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-05.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="positional-encoding注入位置信息"&gt;&lt;a href="#positional-encoding%e6%b3%a8%e5%85%a5%e4%bd%8d%e7%bd%ae%e4%bf%a1%e6%81%af" class="header-anchor"&gt;&lt;/a&gt;Positional Encoding：注入位置信息
&lt;/h2&gt;&lt;p&gt;Self-Attention 本身不天然知道顺序。如果只给模型一组 token 向量，它并不知道哪个 token 在前、哪个在后。&lt;/p&gt;
&lt;p&gt;所以需要位置编码。GPT-2 使用可学习的位置 embedding，把 token 的语义向量和位置向量相加：&lt;/p&gt;
$$
x_i = \text{TokenEmbedding}_i + \text{PositionEmbedding}_i
$$&lt;p&gt;这样模型既知道“这个 token 是什么”，也知道“它在第几个位置”。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 6: Positional Encoding" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-06.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="repetitive-transformer-blocks重复堆叠的-transformer-block"&gt;&lt;a href="#repetitive-transformer-blocks%e9%87%8d%e5%a4%8d%e5%a0%86%e5%8f%a0%e7%9a%84-transformer-block" class="header-anchor"&gt;&lt;/a&gt;Repetitive Transformer Blocks：重复堆叠的 Transformer Block
&lt;/h2&gt;&lt;p&gt;Embedding 只是输入表示，还不是充分理解后的语义表示。真正的上下文建模发生在 Transformer Block 中。&lt;/p&gt;
&lt;p&gt;GPT-2 small 有 12 个 Transformer Block。每个 block 大致包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multi-Head Self-Attention：让 token 之间交换信息。&lt;/li&gt;
&lt;li&gt;MLP：对每个 token 的表示做非线性加工。&lt;/li&gt;
&lt;li&gt;Residual、LayerNorm、Dropout：让训练更稳定，泛化更好。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多层堆叠的意义是：底层更偏局部和词法信息，高层更容易形成复杂语义和任务相关表示。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 7: Repetitive Transformer Blocks" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-07.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="multi-head-self-attention多头自注意力"&gt;&lt;a href="#multi-head-self-attention%e5%a4%9a%e5%a4%b4%e8%87%aa%e6%b3%a8%e6%84%8f%e5%8a%9b" class="header-anchor"&gt;&lt;/a&gt;Multi-Head Self Attention：多头自注意力
&lt;/h2&gt;&lt;p&gt;Self-Attention 的目标是让每个 token 根据上下文更新自己。比如 &lt;code&gt;to&lt;/code&gt; 这个 token 在不同句子里含义不同，它需要“看”前面的 &lt;code&gt;Data visualization empowers users&lt;/code&gt;，才能形成更准确的表示。&lt;/p&gt;
&lt;p&gt;Multi-Head 的含义是：模型不是只用一种注意力视角，而是并行使用多个 head。GPT-2 small 有 12 个 attention heads。不同 head 可以学习不同关系，例如语法关系、短距离搭配、长距离语义依赖等。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 8: Multi-Head Self Attention" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-08.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="query-key-valueqkv-是什么"&gt;&lt;a href="#query-key-valueqkv-%e6%98%af%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;Query, Key, Value：Q、K、V 是什么
&lt;/h2&gt;&lt;p&gt;Self-Attention 会把每个 token 的输入向量分别映射成三个向量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Query (Q)&lt;/strong&gt;：当前 token 想查什么信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key (K)&lt;/strong&gt;：每个 token 能被别人匹配到的特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value (V)&lt;/strong&gt;：真正要被聚合传递的信息内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们来自线性变换：&lt;/p&gt;
$$
Q = XW_Q,\quad K = XW_K,\quad V = XW_V
$$&lt;p&gt;一个通俗类比是搜索引擎：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query 是搜索词。&lt;/li&gt;
&lt;li&gt;Key 是网页标题或索引。&lt;/li&gt;
&lt;li&gt;Value 是网页正文内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先用 Query 和 Key 算相关性，再根据相关性加权读取 Value。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 9: Query, Key, Value" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-09.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="multi-head把-qkv-切成多个头"&gt;&lt;a href="#multi-head%e6%8a%8a-qkv-%e5%88%87%e6%88%90%e5%a4%9a%e4%b8%aa%e5%a4%b4" class="header-anchor"&gt;&lt;/a&gt;Multi-head：把 Q/K/V 切成多个头
&lt;/h2&gt;&lt;p&gt;GPT-2 small 的 embedding 维度是 768，attention head 数是 12，所以每个 head 处理的维度通常是：&lt;/p&gt;
$$
768 / 12 = 64
$$&lt;p&gt;多头机制的好处是并行学习多种关系。一个 head 可能关注相邻词，另一个 head 可能关注主谓关系，还有 head 可能关注更远处的语义提示。&lt;/p&gt;
&lt;p&gt;多个 head 不是重复劳动，而是让模型拥有多个“观察角度”。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 10: Multi-head" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-10.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="masked-self-attention带掩码的自注意力"&gt;&lt;a href="#masked-self-attention%e5%b8%a6%e6%8e%a9%e7%a0%81%e7%9a%84%e8%87%aa%e6%b3%a8%e6%84%8f%e5%8a%9b" class="header-anchor"&gt;&lt;/a&gt;Masked Self Attention：带掩码的自注意力
&lt;/h2&gt;&lt;p&gt;GPT 这类模型是从左到右生成文本的。预测当前位置时，不能偷看未来 token，所以要使用 causal mask，也叫 masked self-attention。&lt;/p&gt;
&lt;p&gt;核心计算公式是：&lt;/p&gt;
$$
\text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + M\right)V
$$&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$QK^T$：计算 token 两两之间的相似度。&lt;/li&gt;
&lt;li&gt;$\sqrt{d_k}$：缩放因子，避免点积值过大导致 softmax 过尖。&lt;/li&gt;
&lt;li&gt;$M$：mask 矩阵，把未来位置设为 $-\infty$。&lt;/li&gt;
&lt;li&gt;softmax：把分数变成概率。&lt;/li&gt;
&lt;li&gt;乘以 $V$：按注意力权重汇总信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Step 11: Masked Self Attention" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-11.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="attention-output--concatenation注意力输出与拼接"&gt;&lt;a href="#attention-output--concatenation%e6%b3%a8%e6%84%8f%e5%8a%9b%e8%be%93%e5%87%ba%e4%b8%8e%e6%8b%bc%e6%8e%a5" class="header-anchor"&gt;&lt;/a&gt;Attention Output &amp;amp; Concatenation：注意力输出与拼接
&lt;/h2&gt;&lt;p&gt;每个 head 都会输出一份上下文增强后的 token 表示。因为 GPT-2 small 有 12 个 head，所以会得到 12 份结果。&lt;/p&gt;
&lt;p&gt;接下来模型会把这些 head 的输出拼接起来，再经过一次线性投影，回到原来的隐藏维度 768：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;head_1, head_2, ..., head_12 -&amp;gt; concat -&amp;gt; linear projection
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一步的意义是：先让多个 head 分别提取信息，再把多种视角融合成一个统一表示。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 12: Attention Output &amp; Concatenation" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-12.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mlp逐-token-的非线性加工"&gt;&lt;a href="#mlp%e9%80%90-token-%e7%9a%84%e9%9d%9e%e7%ba%bf%e6%80%a7%e5%8a%a0%e5%b7%a5" class="header-anchor"&gt;&lt;/a&gt;MLP：逐 token 的非线性加工
&lt;/h2&gt;&lt;p&gt;Attention 负责 token 之间的信息流动，MLP 负责对每个 token 自己的表示进行加工。&lt;/p&gt;
&lt;p&gt;GPT-2 的 MLP 通常包含两层线性变换，中间接 GELU 激活：&lt;/p&gt;
$$
\text{MLP}(x)=W_2\cdot \text{GELU}(W_1x+b_1)+b_2
$$&lt;p&gt;第一层会把维度从 768 扩展到 3072，第二层再压回 768。扩展维度可以让模型在更高维空间中表达更复杂的特征。&lt;/p&gt;
&lt;p&gt;注意：MLP 不像 Attention 那样跨 token 交流信息，它是对每个 token 独立处理。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 13: MLP" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-13.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="output-logit输出-logits"&gt;&lt;a href="#output-logit%e8%be%93%e5%87%ba-logits" class="header-anchor"&gt;&lt;/a&gt;Output Logit：输出 logits
&lt;/h2&gt;&lt;p&gt;经过所有 Transformer Blocks 后，模型会拿最后一个位置的输出向量去预测下一个 token。&lt;/p&gt;
&lt;p&gt;这个向量会经过最终线性层，映射到词表大小：&lt;/p&gt;
$$
\text{logits}=h_{\text{last}}W_{\text{vocab}}+b
$$&lt;p&gt;GPT-2 的词表大小是 50,257，所以 logits 是一个长度为 50,257 的向量。每个数对应一个候选 token 的原始分数。&lt;/p&gt;
&lt;p&gt;logit 不是概率。它可以是任意实数，还需要经过 softmax 才能变成概率分布。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 14: Output Logit" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-14.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="probabilities从-logits-到概率"&gt;&lt;a href="#probabilities%e4%bb%8e-logits-%e5%88%b0%e6%a6%82%e7%8e%87" class="header-anchor"&gt;&lt;/a&gt;Probabilities：从 logits 到概率
&lt;/h2&gt;&lt;p&gt;Softmax 会把 logits 转换成概率：&lt;/p&gt;
$$
p_i=\frac{e^{z_i}}{\sum_j e^{z_j}}
$$&lt;p&gt;转换后有两个特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 token 的概率都在 0 到 1 之间。&lt;/li&gt;
&lt;li&gt;所有 token 的概率加起来等于 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图中可以看到，示例输入后，模型认为 &lt;code&gt;visualize&lt;/code&gt;、&lt;code&gt;create&lt;/code&gt;、&lt;code&gt;see&lt;/code&gt;、&lt;code&gt;make&lt;/code&gt; 等 token 是比较可能的下一个 token。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 15: Probabilities" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-15.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="temperature温度控制生成随机性"&gt;&lt;a href="#temperature%e6%b8%a9%e5%ba%a6%e6%8e%a7%e5%88%b6%e7%94%9f%e6%88%90%e9%9a%8f%e6%9c%ba%e6%80%a7" class="header-anchor"&gt;&lt;/a&gt;Temperature：温度控制生成随机性
&lt;/h2&gt;&lt;p&gt;Temperature 会在 softmax 前缩放 logits：&lt;/p&gt;
$$
p_i=\frac{\exp(z_i/T)}{\sum_j \exp(z_j/T)}
$$&lt;p&gt;其中 $T$ 是 temperature：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$T &amp;lt; 1$：概率分布更尖锐，高分 token 更容易被选中，输出更稳定。&lt;/li&gt;
&lt;li&gt;$T = 1$：不额外调整 logits。&lt;/li&gt;
&lt;li&gt;$T &amp;gt; 1$：概率分布更平坦，低概率 token 也有更多机会被选中，输出更多样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通俗地说，temperature 越低越保守，越高越发散。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 16: Temperature" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-16.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="sampling-strategy采样策略"&gt;&lt;a href="#sampling-strategy%e9%87%87%e6%a0%b7%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;Sampling Strategy：采样策略
&lt;/h2&gt;&lt;p&gt;得到概率分布后，模型还要决定如何选下一个 token。常见策略有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Greedy Search&lt;/strong&gt;：永远选概率最高的 token，稳定但容易死板。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Top-k&lt;/strong&gt;：只保留概率最高的 k 个 token，再从中采样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Top-p&lt;/strong&gt;：保留累计概率达到 p 的最小 token 集合，也叫 nucleus sampling。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Top-k 更像固定候选池，Top-p 更像动态候选池。实际使用时，temperature 和 top-k/top-p 经常一起调。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 17: Sampling Strategy" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-17.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="residual-connection残差连接"&gt;&lt;a href="#residual-connection%e6%ae%8b%e5%b7%ae%e8%bf%9e%e6%8e%a5" class="header-anchor"&gt;&lt;/a&gt;Residual Connection：残差连接
&lt;/h2&gt;&lt;p&gt;残差连接会把某一层的输入直接加到输出上：&lt;/p&gt;
$$
y = x + F(x)
$$&lt;p&gt;它的作用是保留原始信息，并让梯度更容易穿过深层网络。如果没有残差连接，模型层数很深时，训练会更困难，早期层的信息也更容易丢失。&lt;/p&gt;
&lt;p&gt;在 Transformer 中，Attention 和 MLP 周围通常都有残差连接。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 18: Residual Connection" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-18.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="layer-normalization层归一化"&gt;&lt;a href="#layer-normalization%e5%b1%82%e5%bd%92%e4%b8%80%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;Layer Normalization：层归一化
&lt;/h2&gt;&lt;p&gt;Layer Normalization 会对一个 token 向量内部的数值做归一化，使均值和方差更稳定：&lt;/p&gt;
$$
\text{LayerNorm}(x)=\gamma\frac{x-\mu}{\sqrt{\sigma^2+\epsilon}}+\beta
$$&lt;p&gt;它能减少训练不稳定，让每一层输入分布更可控。GPT-2 使用的是 pre-norm 风格：在进入 Attention 和 MLP 前先做 LayerNorm。&lt;/p&gt;
&lt;p&gt;通俗理解：LayerNorm 像是在每次进入关键计算前，先把数值尺度整理到比较合适的范围。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step 19: Layer Normalization" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-19.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="dropout训练时的随机失活"&gt;&lt;a href="#dropout%e8%ae%ad%e7%bb%83%e6%97%b6%e7%9a%84%e9%9a%8f%e6%9c%ba%e5%a4%b1%e6%b4%bb" class="header-anchor"&gt;&lt;/a&gt;Dropout：训练时的随机失活
&lt;/h2&gt;&lt;p&gt;Dropout 是训练阶段的正则化方法，会随机把一部分连接或激活置零，避免模型过度依赖某些局部特征。&lt;/p&gt;
&lt;p&gt;它的直觉是：训练时不要让模型每次都走完全相同的路径，迫使它学到更稳健的表示。&lt;/p&gt;
&lt;p&gt;需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dropout 主要用于训练。&lt;/li&gt;
&lt;li&gt;推理时 Dropout 会关闭。&lt;/li&gt;
&lt;li&gt;很多新一代大模型因为训练数据极大，Dropout 使用得比早期模型更少。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Step 20: Dropout" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/transformer-explainer/steps/step-20.jpg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Transformer Explainer&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一张流程图总结"&gt;&lt;a href="#%e4%b8%80%e5%bc%a0%e6%b5%81%e7%a8%8b%e5%9b%be%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;一张流程图总结
&lt;/h2&gt;&lt;p&gt;可以把 GPT 类 Transformer 的推理流程压缩成下面这条链路：&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A["输入文本"] --&gt; B["Tokenization"]
 B --&gt; C["Token Embedding"]
 C --&gt; D["Positional Encoding"]
 D --&gt; E["Transformer Block x N"]
 E --&gt; F["Multi-Head Self-Attention"]
 F --&gt; G["MLP"]
 G --&gt; H["Final Linear"]
 H --&gt; I["Logits"]
 I --&gt; J["Softmax Probabilities"]
 J --&gt; K["Temperature / Top-k / Top-p"]
 K --&gt; L["采样下一个 token"]&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="和-rnn-的关键区别"&gt;&lt;a href="#%e5%92%8c-rnn-%e7%9a%84%e5%85%b3%e9%94%ae%e5%8c%ba%e5%88%ab" class="header-anchor"&gt;&lt;/a&gt;和 RNN 的关键区别
&lt;/h2&gt;&lt;p&gt;结合之前的 RNN 学习，可以这样理解二者差异：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;对比点&lt;/th&gt;
 &lt;th&gt;RNN&lt;/th&gt;
 &lt;th&gt;Transformer&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;信息传递方式&lt;/td&gt;
 &lt;td&gt;依赖隐藏状态逐步传递&lt;/td&gt;
 &lt;td&gt;Self-Attention 让 token 直接互相读取&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;并行能力&lt;/td&gt;
 &lt;td&gt;时间步依赖强，难并行&lt;/td&gt;
 &lt;td&gt;同层 token 可并行计算&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;长距离依赖&lt;/td&gt;
 &lt;td&gt;路径长，容易衰减&lt;/td&gt;
 &lt;td&gt;任意位置可直接建立联系&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;上下文表示&lt;/td&gt;
 &lt;td&gt;压缩进隐藏状态&lt;/td&gt;
 &lt;td&gt;显式保留整段 token 表示&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;大模型训练&lt;/td&gt;
 &lt;td&gt;扩展效率较差&lt;/td&gt;
 &lt;td&gt;更适合 GPU/TPU 大规模矩阵计算&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这也是为什么现代 LLM 主流选择 Transformer：它不仅建模能力强，而且工程上更适合大规模预训练。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="学习来源"&gt;&lt;a href="#%e5%ad%a6%e4%b9%a0%e6%9d%a5%e6%ba%90" class="header-anchor"&gt;&lt;/a&gt;学习来源
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://poloclub.github.io/transformer-explainer/" target="_blank" rel="noopener"
 &gt;Transformer Explainer: LLM Transformer Model Visually Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/poloclub/transformer-explainer" target="_blank" rel="noopener"
 &gt;Transformer Explainer GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://arxiv.org/abs/2408.04619" target="_blank" rel="noopener"
 &gt;Transformer Explainer Paper, arXiv:2408.04619&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Vaswani et al., &lt;a class="link" href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener"
 &gt;Attention Is All You Need&lt;/a&gt;, 2017&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>