<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Azure on Wilson Wu</title><link>https://wilsonwu.me/zh/tags/azure/</link><description>Recent content in Azure on Wilson Wu</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Sat, 10 Oct 2026 00:00:00 -0400</lastBuildDate><atom:link href="https://wilsonwu.me/zh/tags/azure/index.xml" rel="self" type="application/rss+xml"/><item><title>Microsoft-Decision-1 详解：微软入局决策模型，与 Jev、Laya 的三方对比</title><link>https://wilsonwu.me/zh/blog/2026/microsoft-decision-1/</link><pubDate>Sat, 10 Oct 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/microsoft-decision-1/</guid><description>&lt;p&gt;&lt;img alt="Microsoft-Decision-1、Jev 与 Laya" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/microsoft-decision-1/1-microsoft-decision-1.png"&gt;&lt;/p&gt;
&lt;h2 id="引言"&gt;引言&lt;/h2&gt;
&lt;p&gt;上个月我写了一篇 &lt;a href="https://wilsonwu.me/zh/blog/2026/jev-vs-laya/"&gt;Jev vs Laya&lt;/a&gt;，对比了 TypeSafe AI 的闭源决策模型 Jev 和社区开源的 Laya。那时“决策模型”还是一个刚刚诞生的新品类：9 月 15 日 Jev 发布，三天后 Laya 开源。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="Microsoft-Decision-1、Jev 与 Laya" loading="lazy" src="/zh/blog/2026/microsoft-decision-1/1-microsoft-decision-1.png"></p>
<h2 id="引言">引言</h2>
<p>上个月我写了一篇 <a href="/zh/blog/2026/jev-vs-laya/">Jev vs Laya</a>，对比了 TypeSafe AI 的闭源决策模型 Jev 和社区开源的 Laya。那时“决策模型”还是一个刚刚诞生的新品类：9 月 15 日 Jev 发布，三天后 Laya 开源。</p>
<p>仅仅三周之后，这个赛道就挤满了玩家：10 月 6 日，OpenAI 开放了基于 GPT-6 Luna 的 Decisions API；Liquid AI、Inception 等公司也陆续推出了自己的决策 API；第三方榜单 JevBench 上参与排名的开源权重系统已经超过 140 个。10 月 9 日，微软也正式入局，发布了 <strong>Microsoft-Decision-1</strong>。</p>
<p>和其他入局者相比，Microsoft-Decision-1 有几个特别值得关注的地方：</p>
<ul>
<li><strong>出自大厂，底座却是开源模型</strong>：它基于阿里开源的 Qwen3.5-9B 后训练而来，微软还表示后续会迁移到自家的 MAI 系列和 OpenAI 的模型上；</li>
<li><strong>价格与 Jev 完全一致</strong>：输入每百万 token 0.042 美元，输出免费；</li>
<li><strong>接口几乎与 Jev 兼容</strong>：同样是 state + questions，同样是 noul / choice / score 三种问题类型；</li>
<li><strong>官方数据很亮眼</strong>：在 36 项基准、近 15 万道题的对比中平均准确率最高，中位延迟比 GPT-6 Sol 快约 35 倍。</li>
</ul>
<p>但官方数据只是故事的一半。微软发布的第二天，第三方榜单 JevBench 就完成了独立实测，结论并不完全一致。本文将从以下几个方面展开：</p>
<ol>
<li>Microsoft-Decision-1 是什么，又是怎么做出来的</li>
<li>它与 Jev、Laya 的技术路线和接口有什么不同</li>
<li>官方基准与第三方实测应该怎么看</li>
<li>Microsoft-Decision-1 的优势与劣势</li>
<li>适用场景、选型建议与代码示例</li>
</ol>
<h2 id="一分钟回顾什么是决策模型">一分钟回顾：什么是决策模型</h2>
<p>决策模型（System One Model）不聊天，也不写文章，它只做一件事：<strong>接收一段状态（state）和一组预先定义好的问题（questions），直接返回带概率的类型化决策</strong>。三家都支持同样的三种问题原语：</p>
<table>
	<thead>
			<tr>
					<th>原语</th>
					<th>回答的问题</th>
					<th>返回内容</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>noul</code></td>
					<td>是不是？</td>
					<td>“是”的概率（0～1）</td>
			</tr>
			<tr>
					<td><code>choice</code></td>
					<td>选哪一个？</td>
					<td>选中的选项，以及每个选项的概率</td>
			</tr>
			<tr>
					<td><code>score</code></td>
					<td>有多少？</td>
					<td>有序等级上的分数，以及每个等级的概率</td>
			</tr>
	</tbody>
</table>
<p>由于答案空间被提前限定，模型不会返回格式错误或凭空编造的选项；校准过的概率则可以直接写进程序的 <code>if</code> 语句：高置信度自动执行，中置信度交给更强的模型复核，低置信度转人工。在一个 AI 系统里，比较合理的分工是：<strong>代码负责骨架，决策模型负责快速判断，大模型负责深度思考</strong>。</p>
<p>更详细的概念介绍和置信度分流的思路，可以参考上一篇，这里不再展开。</p>
<h2 id="microsoft-decision-1-是什么">Microsoft-Decision-1 是什么</h2>
<h3 id="1-基本信息">1. 基本信息</h3>
<p>2026 年 10 月 9 日，微软首席技术官办公室（Office of the CTO）软件工程副总裁 Achint Srivastava 在微软的技术博客 Command Line 上发布了 Microsoft-Decision-1。文章开头是这样定义决策模型的：</p>

<blockquote>
  <p>Unlike LLMs, which are designed to generate text or reason through complex problems, decision models are purpose-built to deliver structured outputs that software can immediately act on.</p>
<p>与生成文本、推理复杂问题的大模型不同，决策模型专门用来输出软件可以立即据此行动的结构化结果。</p>

</blockquote>
<p>微软给它的定位是一个“快速决策打分”（fast decision-scoring）模型，面向路由、分类、优先级排序、结果校验和工作流控制。主要信息如下：</p>
<table>
	<thead>
			<tr>
					<th>项目</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>发布时间</td>
					<td>2026 年 10 月 9 日</td>
			</tr>
			<tr>
					<td>发布方</td>
					<td>微软首席技术官办公室（Office of the CTO）</td>
			</tr>
			<tr>
					<td>底座模型</td>
					<td>Qwen3.5-9B（后训练），计划迁移到 MAI 和 OpenAI 的模型</td>
			</tr>
			<tr>
					<td>模型类型</td>
					<td>决策、文本分类、零样本分类</td>
			</tr>
			<tr>
					<td>问题类型</td>
					<td><code>noul</code>（是 / 否）、<code>choice</code>（多选一）、<code>score</code>（有序打分），以及基于评分细则（rubric）评估 AI 回答和 Agent 行为</td>
			</tr>
			<tr>
					<td>输入</td>
					<td>仅支持文本（字符串或 JSON），单次请求最多 32K token，不支持图像、音频和视频</td>
			</tr>
			<tr>
					<td>输出</td>
					<td>选中项和每个候选项的校准概率，不生成解释或理由</td>
			</tr>
			<tr>
					<td>获取方式</td>
					<td>Microsoft Foundry 模型目录（模型名 <code>Microsoft-Decision-1</code>，版本 <code>1</code>），也可以通过 OpenRouter 调用</td>
			</tr>
			<tr>
					<td>部署类型</td>
					<td><code>GlobalStandard</code>，部分区域支持 <code>DataZoneStandard</code></td>
			</tr>
			<tr>
					<td>调用端点</td>
					<td><code>{Foundry 资源端点}/providers/microsoft/v1/systemone</code></td>
			</tr>
			<tr>
					<td>认证方式</td>
					<td>Microsoft Entra ID（推荐）或 API Key</td>
			</tr>
			<tr>
					<td>价格</td>
					<td>输入 0.042 美元 / 百万 token，输出免费</td>
			</tr>
	</tbody>
</table>
<p>按这个价格，如果每次判断平均消耗 1000 个输入 token，100 万次判断的费用约为 42 美元，和 Jev 完全相同。</p>
<h3 id="2-它是怎么做出来的">2. 它是怎么做出来的</h3>
<p>微软公开的技术细节不多，但有几个关键点：</p>
<ul>
<li><strong>在开源底座上后训练</strong>：微软在 Qwen3.5-9B 上做了后训练（post-training），让它学会“单次前向完成决策打分”（fast, single-pass decision scoring）。给定一组固定的候选答案，模型直接为每个选项输出一个校准过的概率，而不是逐 token 生成文字。</li>
<li><strong>底座可以替换</strong>：微软明确表示，很快会把它迁移（rebase）到其他底座上，包括微软自研的 MAI 系列和 OpenAI 的模型。</li>
<li><strong>没有公开的部分</strong>：训练数据、后训练方法和打分机制的具体实现都没有公开，权重也没有开放，只能以托管 API 的形式使用。</li>
</ul>
<p><img alt="Microsoft-Decision-1 的工作方式" loading="lazy" src="/zh/blog/2026/microsoft-decision-1/2-how-decision-1-works.png"></p>
<p>微软在博客里把打造一个可靠决策模型的难点归纳为五个方面，并给出了相应的自测结果：</p>
<table>
	<thead>
			<tr>
					<th>挑战</th>
					<th>为什么重要</th>
					<th>微软的自测结果</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>速度</td>
					<td>Agent 工作流中的决策往往是串行的，每个决策多 100 毫秒，20 个串行决策就会多出 2 秒</td>
					<td>P50 延迟约为 GPT-6 Sol 的 1/35</td>
			</tr>
			<tr>
					<td>泛化</td>
					<td>模型很容易在单个基准上过拟合，必须验证它在没见过的任务上是否同样可靠</td>
					<td>在数十个对训练保密的基准（路由、排序、长上下文、多语言、分布外任务、推理、安全）上评测；与 JevBench 榜单上的头部模型在另外 36 个公开和私有基准上对比，平均准确率最高</td>
			</tr>
			<tr>
					<td>鲁棒性</td>
					<td>生产环境里，state 和指令会被改写，选项会被重排，字段名会变化，这些等价变化不应该改变决策</td>
					<td>对同一请求做 8 种扰动，平均决策翻转率为 1.3%；改写选项描述、颠倒或打乱选项顺序时，翻转率为 0</td>
			</tr>
			<tr>
					<td>校准</td>
					<td>概率本身就是 API 的一部分，应用要依据它决定是执行、推迟还是送审，“90% 的把握”应该在代表性样本上 10 次对 9 次</td>
					<td>36 项基准上的校准分为 92.2（满分 100），在参评的 7 个模型中排第三，比 Jev 低 1.5 分</td>
			</tr>
			<tr>
					<td>安全</td>
					<td>既要识别有害请求，又不能误伤正常请求</td>
					<td>在 11 个基准、5250 条请求（有害内容、越狱攻击、提示词注入）上测试，能够拒绝有害行为，同时保持较高的可用性</td>
			</tr>
	</tbody>
</table>
<h3 id="3-微软内部的四个试点">3. 微软内部的四个试点</h3>
<p>微软还公开了几个内部试用的案例（均为微软自测）：</p>
<table>
	<thead>
			<tr>
					<th>团队 / 场景</th>
					<th>任务</th>
					<th>结果</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Xbox Research：数据标注</td>
					<td>把来自问卷、Steam 和 Twitter/X 的 1 万多条开放式反馈，归入研究员预先定义好的主题</td>
					<td>质量与 GPT-6 Sol 相当，速度快 14 倍以上，成本低约 200 倍</td>
			</tr>
			<tr>
					<td>Copilot：质量控制</td>
					<td>评估聊天回答和 Agent 回答的质量</td>
					<td>与 GPT-5.6 Luna 表现相当，速度快约 100 倍</td>
			</tr>
			<tr>
					<td>事故响应</td>
					<td>值班工程师处理线上事故时，从日志、工单、通话和消息中检索相关知识</td>
					<td>比基于大模型的方案更准、更快</td>
			</tr>
			<tr>
					<td>Microsoft Discovery：科学研究</td>
					<td>Agent 按评分细则评估上一轮实验的结果，调整方案并反复迭代，直到完成目标</td>
					<td>打分一致性是大模型打分的 46 倍，速度是 3 倍，自适应重规划整体提速近 4 倍</td>
			</tr>
	</tbody>
</table>
<p>其中 Xbox 的三个数据集，微软给出了更详细的对比数据：</p>
<table>
	<thead>
			<tr>
					<th>数据集</th>
					<th>样本量</th>
					<th>平均每条耗时（GPT-6 Sol → Decision-1）</th>
					<th>跑完一遍的成本（GPT-6 Sol → Decision-1）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>某赛车游戏的评论</td>
					<td>753 条</td>
					<td>2.8 秒 → 159 毫秒（约 18 倍）</td>
					<td>1.83 美元 → 0.009 美元（约 213 倍）</td>
			</tr>
			<tr>
					<td>某第一人称射击游戏的评论</td>
					<td>341 条</td>
					<td>2.6 秒 → 188 毫秒（约 14 倍）</td>
					<td>1.03 美元 → 0.005 美元（约 212 倍）</td>
			</tr>
			<tr>
					<td>某工作室直播的帖子</td>
					<td>1030 条</td>
					<td>2.6 秒 → 143 毫秒（约 18 倍）</td>
					<td>2.92 美元 → 0.013 美元（约 223 倍）</td>
			</tr>
	</tbody>
</table>
<p>按 100 万条文本折算，GPT-6 Sol 约需 2434 美元，Microsoft-Decision-1 约需 11 美元（微软注明成本为估算值）。两者读取的是同一份任务说明，输入 token 数相同，差距来自单价，以及 GPT-6 Sol 还需要为输出付费。</p>
<h3 id="4-适合哪些场景">4. 适合哪些场景</h3>
<p>微软在博客里列出了 16 个推荐场景，大致可以归为四类：</p>
<table>
	<thead>
			<tr>
					<th>类别</th>
					<th>场景</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Agent 控制</td>
					<td>评估 Agent 提出的下一步，决定继续、停止、重试，还是交给其他模型、工具或人；按照 Skill 中的规则选择下一步动作，而不必反复处理一长串指令；在电脑和界面操作中选择下一个 UI 动作；在机器人场景中从预定义的动作里做选择</td>
			</tr>
			<tr>
					<td>路由与分类</td>
					<td>模型路由、意图识别、事故分级与派单、内容分类与过滤、数据标注</td>
			</tr>
			<tr>
					<td>评估与校验</td>
					<td>AI 评审（按标准决定接受、修改还是拒绝一个回答）、数据校验、代码扫描、安全风险筛查（放行、拦截或升级）</td>
			</tr>
			<tr>
					<td>排序与推荐</td>
					<td>推荐、搜索相关性打分，以及在科学研究中筛选假设、化合物和实验</td>
			</tr>
	</tbody>
</table>
<p>其中最值得关注的是 <strong>Agent 控制</strong>。今天的 Agent 每走一步都要做大量“小判断”：上一步成功了吗？要不要重试？这个工具调用会不会动到生产数据？如果这些判断都交给大模型，延迟和成本会随着步数线性累积。用一个百毫秒级的决策模型给 Agent 加一道“闸门”，是这类模型最有想象空间的用法。微软还演示了一个电脑操作（computer use）的例子：在完成“买一个背包”的任务时，由 Microsoft-Decision-1 选择下一步界面操作，完成速度明显快于 GPT-6 Sol。</p>
<p><img alt="用决策模型给 Agent 加一道闸门" loading="lazy" src="/zh/blog/2026/microsoft-decision-1/3-agent-decision-gate.png"></p>
<p>另一个值得一提的场景是<strong>模型路由</strong>：根据请求的质量、成本和延迟要求，选择最合适的模型来处理。我之前写过一篇 <a href="/zh/blog/2026/llm-smart-router/">让模型自己选模型：Embedding 驱动的 LLM 智能路由机制</a>，决策模型提供了另一种思路：不再依赖向量相似度，而是直接把“该交给哪个模型”当成一道 <code>choice</code> 题来回答。</p>
<h2 id="三条技术路线">三条技术路线</h2>
<p><img alt="三种决策模型的技术路线" loading="lazy" src="/zh/blog/2026/microsoft-decision-1/4-three-architectures.png"></p>
<p>把三个模型放在一起，会发现它们代表了三条截然不同的技术路线：</p>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>Jev</th>
					<th>Laya</th>
					<th>Microsoft-Decision-1</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>模型结构</td>
					<td>未公开（社区猜测接近 encoder 类模型）</td>
					<td>双向编码器（ModernBERT / mmBERT）+ 决策头</td>
					<td>解码器大模型 Qwen3.5-9B 后训练</td>
			</tr>
			<tr>
					<td>参数规模</td>
					<td>未公开</td>
					<td>322M～421M</td>
					<td>约 9B</td>
			</tr>
			<tr>
					<td>训练方法</td>
					<td>RLCD（面向校准决策的强化学习，细节未公开）</td>
					<td>RLCD（严格适当评分规则 + 组基线策略梯度，已公开）</td>
					<td>面向单次决策打分的后训练（细节未公开）</td>
			</tr>
			<tr>
					<td>推理方式</td>
					<td>自研的并行采样器</td>
					<td>每个选项前放一个 <code>[MASK]</code>，单次前向打分</td>
					<td>单次前向打分</td>
			</tr>
			<tr>
					<td>权重</td>
					<td>闭源</td>
					<td>Apache 2.0 开源</td>
					<td>不开放（底座开源）</td>
			</tr>
	</tbody>
</table>
<p>有几点值得展开说说：</p>
<ol>
<li><strong>规模相差一个数量级以上</strong>。Laya 用 4 亿参数的编码器换取极致的速度和本地部署能力；Microsoft-Decision-1 用 90 亿参数的解码器换取更强的零样本泛化能力，代价是对算力的要求高得多，而且目前只能以云端托管的方式使用。</li>
<li><strong>底座正在变成可替换的零件</strong>。从 JevBench 榜单看，排名靠前的开源决策模型大多也是在 Qwen 或 Gemma 上后训练出来的，比如基于 Qwen3.5-4B 的 H2O-Lightning-4B、基于 Gemma-4-31B 的 Quyet-1.0-Large。微软直接选用阿里的开源模型做底座，又公开表示后续会换成 MAI 或 OpenAI 的模型，说明这类模型的竞争力主要来自后训练数据、评测体系和校准方法，而不是底座本身。</li>
<li><strong>“单次前向”已经是共识</strong>。无论是 Laya 的编码器还是 Microsoft-Decision-1 的解码器，核心思路都一样：不生成文本，一次前向计算就给出所有候选项的概率。这也是决策模型能比通用大模型快一到两个数量级的根本原因。</li>
</ol>
<h2 id="同一套接口三种后端">同一套接口，三种后端</h2>
<p><img alt="同一组请求，三种后端" loading="lazy" src="/zh/blog/2026/microsoft-decision-1/5-same-api-three-backends.png"></p>
<p>对开发者来说，一个非常实用的变化是：<strong>三者的请求格式几乎一致</strong>。</p>
<ul>
<li>Jev 的官方 API 是 <code>POST https://api.typesafe.ai/v1/systemone</code>；</li>
<li>Microsoft-Decision-1 的端点是 Foundry 资源下的 <code>/providers/microsoft/v1/systemone</code>，微软文档也直接说明，它的请求模式与 System One API 类似；</li>
<li>Laya 也提供了 <code>laya-serve</code>，在本地以同样的 <code>POST /v1/systemone</code> 协议对外服务，返回结构与 Jev 一致，已有的 Jev 客户端只需要修改 <code>baseUrl</code>。</li>
</ul>
<p>三者在细节上的差异如下：</p>
<table>
	<thead>
			<tr>
					<th>项目</th>
					<th>Microsoft-Decision-1</th>
					<th>Jev</th>
					<th>Laya（laya-serve）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>端点</td>
					<td><code>{Foundry 资源端点}/providers/microsoft/v1/systemone</code></td>
					<td><code>https://api.typesafe.ai/v1/systemone</code></td>
					<td><code>http://{主机}:8000/v1/systemone</code></td>
			</tr>
			<tr>
					<td>认证</td>
					<td>Entra ID 的 Bearer Token，或 <code>api-key</code> 请求头</td>
					<td><code>Authorization: Bearer {API Key}</code></td>
					<td>默认无认证，设置 <code>LAYA_API_KEY</code> 后使用 Bearer Token</td>
			</tr>
			<tr>
					<td><code>model</code> 字段</td>
					<td>部署名称（deployment name）</td>
					<td>模型版本或别名，如 <code>jev-1.13.0</code></td>
					<td>检查点名称，省略时由 Router 自动选择</td>
			</tr>
			<tr>
					<td>单次请求上限</td>
					<td>32K token</td>
					<td>64K token，其中 state 加最长的单个问题不超过 32K</td>
					<td>默认 512 / 1024 token，多语言检查点最高 8192</td>
			</tr>
			<tr>
					<td>批量</td>
					<td>同一个 state 可以带多个问题</td>
					<td>同左</td>
					<td>同左，另有 <code>/v1/systemone/batch</code>，一次最多 64 个 state</td>
			</tr>
	</tbody>
</table>
<p>这意味着 <code>state + questions</code> 风格的接口正在成为决策模型的事实标准，就像当年 OpenAI 的 Chat Completions 接口之于大模型。微软把价格定得和 Jev 一模一样、接口也几乎一致，很难不让人认为这是在刻意降低 Jev 用户的迁移门槛。对用户来说，切换成本越来越低；对厂商来说，护城河只能来自效果、延迟、单次成本、合规能力和生态。</p>
<h2 id="核心差异一览">核心差异一览</h2>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>Microsoft-Decision-1</th>
					<th>Jev</th>
					<th>Laya</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>发布方</td>
					<td>微软</td>
					<td>TypeSafe AI（创业公司）</td>
					<td>Convai Innovations（独立研究者）</td>
			</tr>
			<tr>
					<td>发布时间</td>
					<td>2026 年 10 月 9 日</td>
					<td>2026 年 9 月 15 日</td>
					<td>2026 年 9 月 18 日</td>
			</tr>
			<tr>
					<td>开源</td>
					<td>权重不开放</td>
					<td>闭源</td>
					<td>Apache 2.0</td>
			</tr>
			<tr>
					<td>部署</td>
					<td>Azure 托管（Foundry），也可经 OpenRouter 调用</td>
					<td>TypeSafe 云端（美国西海岸）</td>
					<td>本地、私有云或离线环境</td>
			</tr>
			<tr>
					<td>架构与规模</td>
					<td>Qwen3.5-9B 后训练</td>
					<td>未公开</td>
					<td>编码器 + 决策头，322M～421M</td>
			</tr>
			<tr>
					<td>问题原语</td>
					<td>noul / choice / score + 评分细则</td>
					<td>noul / choice / score</td>
					<td>noul / choice / score</td>
			</tr>
			<tr>
					<td>上下文</td>
					<td>32K token</td>
					<td>64K token</td>
					<td>默认 512 / 1024 token</td>
			</tr>
			<tr>
					<td>零样本能力</td>
					<td>强</td>
					<td>强</td>
					<td>弱，需要微调</td>
			</tr>
			<tr>
					<td>定制方式</td>
					<td>调整 state、instructions 和选项</td>
					<td>调整 state、instructions 和 criteria</td>
					<td>可在自有数据上微调，并拟合校准温度</td>
			</tr>
			<tr>
					<td>官方延迟</td>
					<td>同区域 p50 约 85 ms</td>
					<td>70～500 ms</td>
					<td>T4 上单个问题约 33 ms</td>
			</tr>
			<tr>
					<td>价格</td>
					<td>输入 $0.042 / 百万 token，输出免费</td>
					<td>同左</td>
					<td>模型免费，自备算力</td>
			</tr>
			<tr>
					<td>企业集成</td>
					<td>Azure 订阅计费、Entra ID、RBAC、数据区域部署</td>
					<td>API Key，零数据保留仅面向企业客户</td>
					<td>完全自主可控</td>
			</tr>
			<tr>
					<td>多语言</td>
					<td>未列出支持的语言，评测中包含多语言任务</td>
					<td>以英语为主</td>
					<td>多语言检查点覆盖 100 多种语言</td>
			</tr>
	</tbody>
</table>
<h2 id="基准数据怎么看">基准数据怎么看</h2>
<h3 id="1-微软公布的对比数据">1. 微软公布的对比数据</h3>
<p>微软把 Microsoft-Decision-1 和 JevBench 榜单上的几个头部模型、Jev 以及 GPT-6 Sol 放在一起，在 36 个公开和私有基准、共 147,137 道题上做了对比（准确率和校准分越高越好，延迟越低越好）：</p>
<table>
	<thead>
			<tr>
					<th>模型</th>
					<th>平均准确率</th>
					<th>中位延迟（p50）</th>
					<th>校准分（100 为完美）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Microsoft-Decision-1</strong></td>
					<td><strong>83.5%</strong></td>
					<td><strong>85 ms</strong></td>
					<td>92.2</td>
			</tr>
			<tr>
					<td>Jev 1.13.0</td>
					<td>82.3%</td>
					<td>240 ms</td>
					<td><strong>93.7</strong></td>
			</tr>
			<tr>
					<td>Quyet-1.0-Large</td>
					<td>81.9%</td>
					<td>380 ms</td>
					<td>93.1</td>
			</tr>
			<tr>
					<td>Surogate Rune 26B-A4B</td>
					<td>79.7%</td>
					<td>380 ms</td>
					<td>91.8</td>
			</tr>
			<tr>
					<td>GPT-6 Luna Decisions</td>
					<td>79.4%</td>
					<td>300 ms</td>
					<td>89.9</td>
			</tr>
			<tr>
					<td>deck-31B</td>
					<td>77.8%</td>
					<td>400 ms</td>
					<td>83.5</td>
			</tr>
			<tr>
					<td>H2O-Lightning-4B v1.1</td>
					<td>77.2%</td>
					<td>210 ms</td>
					<td>91.8</td>
			</tr>
			<tr>
					<td>GPT-6 Sol（参照）</td>
					<td>未参与排名</td>
					<td>3010 ms</td>
					<td>未评分</td>
			</tr>
	</tbody>
</table>
<p>注：Microsoft-Decision-1 的延迟是在 Foundry 同区域测得的，其余模型的延迟引用自 JevBench v1.6.1 的调整后中位延迟（10 月 7 日数据）。另外，AWS 的 Strands-Decider 2B 只能回答 36 个基准中的 23 个，平均准确率为 54.8%，未列入上表。微软的文章在发布后还专门更新了一次，补充了 Jev 的准确率和校准数据。</p>
<p>按这组数据，Microsoft-Decision-1 的平均准确率第一，速度最快：比第二快的 H2O-Lightning-4B v1.1 快 2.5 倍，比 Jev 快约 2.8 倍，比 GPT-6 Sol 快约 35 倍；校准分排第三。</p>
<p>需要注意的是，微软说的是 36 项基准的<strong>平均</strong>准确率最高，而不是每一项都排第一，一些报道中“36 项基准全部第一”的说法并不准确。</p>
<h3 id="2-第三方-jevbench-的实测">2. 第三方 JevBench 的实测</h3>
<p>JevBench 是一个独立于 TypeSafe 的社区榜单（个人维护的开源项目 Benchmark Heaven），用 1500 道决策题（1200 道封存题加 300 道公开题），从智能（Intelligence）、校准（Calibration）、速度和成本四个维度评测决策模型。Microsoft-Decision-1 发布的第二天（10 月 10 日），JevBench 就通过 Azure Foundry 的原生接口完成了完整测试。下面是 API 榜单上的部分结果：</p>
<table>
	<thead>
			<tr>
					<th>综合排名</th>
					<th>系统</th>
					<th>综合分</th>
					<th>能力分</th>
					<th>每千次决策成本</th>
					<th>中位延迟</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>Sage 1.3.0（Levanto Labs）</td>
					<td>74.0</td>
					<td>78.6</td>
					<td>约 $0.025</td>
					<td>0.15 s</td>
			</tr>
			<tr>
					<td>2</td>
					<td>d1（Liquid AI）</td>
					<td>73.0</td>
					<td>74.4</td>
					<td>$0.017</td>
					<td>0.27 s</td>
			</tr>
			<tr>
					<td>3</td>
					<td>Mercury Decide（Inception）</td>
					<td>72.4</td>
					<td>73.6</td>
					<td>约 $0.018</td>
					<td>0.33 s</td>
			</tr>
			<tr>
					<td>4</td>
					<td><strong>Jev 1.13.0</strong></td>
					<td><strong>71.5</strong></td>
					<td><strong>77.1</strong></td>
					<td>$0.032</td>
					<td>0.24 s</td>
			</tr>
			<tr>
					<td>5</td>
					<td>wity-1</td>
					<td>70.8</td>
					<td>79.1</td>
					<td>$0.024</td>
					<td>1.57 s</td>
			</tr>
			<tr>
					<td>6</td>
					<td><strong>Microsoft-Decision-1</strong></td>
					<td><strong>69.1</strong></td>
					<td><strong>70.8</strong></td>
					<td>$0.019</td>
					<td>0.46 s</td>
			</tr>
			<tr>
					<td>8</td>
					<td>OpenAI Decisions（gpt-6-luna）</td>
					<td>62.5</td>
					<td>73.5</td>
					<td>$0.052</td>
					<td>0.30 s</td>
			</tr>
	</tbody>
</table>
<p>把 Jev 和 Microsoft-Decision-1 单独拿出来比较：</p>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>Jev 1.13.0</th>
					<th>Microsoft-Decision-1</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>智能分</td>
					<td><strong>63.6</strong></td>
					<td>57.2</td>
			</tr>
			<tr>
					<td>校准分</td>
					<td><strong>90.6</strong></td>
					<td>84.4</td>
			</tr>
			<tr>
					<td>每千次决策成本</td>
					<td>$0.032</td>
					<td><strong>$0.019</strong></td>
			</tr>
			<tr>
					<td>中位延迟</td>
					<td><strong>0.24 s</strong></td>
					<td>0.46 s</td>
			</tr>
	</tbody>
</table>
<p>JevBench 的智能分是扣除随机猜测之后的得分（0 表示和随机猜测相当，100 表示全部答对），能力分是智能分和校准分的平均值，综合分则把智能、校准、速度和成本四个维度放在一起计算。它们和微软表格里的“平均准确率”不是同一个指标，不能直接换算。</p>
<h3 id="3-解读数据时必须注意的五点">3. 解读数据时必须注意的五点</h3>
<p><strong>第一，自测与第三方的结论并不一致。</strong> 在微软的 36 项基准上，Microsoft-Decision-1 的平均准确率比 Jev 高 1.2 个百分点；但在 JevBench 上，Jev 的智能分（63.6 对 57.2）和能力分（77.1 对 70.8）都明显领先。两边的题目、评分方法和统计口径完全不同，谁也不能代表“绝对真相”。比较稳妥的理解是：Microsoft-Decision-1 已经进入第一梯队，但还谈不上全面超越 Jev。这也再次说明，<strong>最可靠的基准永远是你自己的数据</strong>。</p>
<p><strong>第二，校准仍是 Jev 的强项。</strong> 即使在微软自己的数据里，Microsoft-Decision-1 的校准分（92.2）也落后于 Jev（93.7）和 Quyet-1.0-Large（93.1）；在 JevBench 上差距更大（84.4 对 90.6）。对于依赖置信度阈值做分流的系统来说，校准质量往往比准确率高出一两个点更重要。</p>
<p><strong>第三，延迟主要取决于部署位置。</strong> 微软的 85 ms 是在 Foundry 同区域测得的，而表中其他模型的延迟来自 JevBench 自己的测试环境。JevBench 原生调用 Microsoft-Decision-1 测得的中位延迟是 459 ms，在同一个榜单上，Jev 是 240 ms。两组数字都没有错，只是测量条件不同。决策模型的计算本身只需要几十毫秒，真正决定体验的是网络距离：把 Foundry 资源建在离应用最近的区域，对数据驻留有要求时选择 <code>DataZoneStandard</code>，并且一定要用真实流量实测。</p>
<p><strong>第四，同样的单价，不同的单次成本。</strong> 两者的标价都是每百万输入 token 0.042 美元，但 JevBench 在同一批题目上实测的每千次决策成本，Microsoft-Decision-1 约为 0.019 美元，Jev 约为 0.032 美元，前者便宜约 40%。差异来自两者对同样的输入计出的 token 数不同。调用量很大时，这个差距值得算进总成本。</p>
<p><strong>第五，Laya 不在同一条赛道上。</strong> 微软的对比中没有 Laya。JevBench 对 Laya 的三个检查点做了零样本测试，智能分只有 0.3～5.5，几乎等同于随机猜测。这和 Laya 作者自己的定位是一致的：它是一个需要在自有数据上微调的快速底座，而不是零样本决策引擎。再加上默认只有 512 / 1024 token 的上下文，Laya 在 JevBench 这种包含长文本、覆盖开放领域的考卷上吃亏是必然的。它的价值在于本地部署，以及特化之后的速度和成本，这一点在上一篇中已经详细讨论过。</p>
<p>此外，JevBench 中有 23 道约 7.7 万～8.1 万 token 的超长题。Microsoft-Decision-1 有 21 道因为超出上下文限制而失败，Jev 也拒绝了这 23 道超出其输入范围的题目，它们都按答错计分。</p>
<h3 id="4-小结">4. 小结</h3>
<p>比较公允的结论是：<strong>Microsoft-Decision-1 进入了决策模型的第一梯队，优势在于同区域延迟、单次成本和微软生态；Jev 在校准和长上下文上仍然领先；Laya 则是另一条赛道上的选手，适合有数据、需要本地化的团队。</strong></p>
<h2 id="microsoft-decision-1-的优势与劣势">Microsoft-Decision-1 的优势与劣势</h2>
<h3 id="优势">优势</h3>
<ol>
<li><strong>企业级集成</strong>：作为 Azure 直接销售的 Foundry 模型，它通过 Azure 订阅计费，受 Azure SLA 保障并由微软提供支持；支持 Entra ID 认证和 RBAC 权限控制，可以和现有的 Azure 网络、监控、合规体系一起管理。对已经在用 Azure 的企业来说，几乎不需要引入新的供应商。</li>
<li><strong>同区域延迟低</strong>：微软在同区域测得的 p50 约为 85 ms、p95 约为 125 ms，加上决策只需要一次前向计算，适合放进 Agent 的多步循环里。</li>
<li><strong>零样本能力进入第一梯队</strong>：不需要任何训练数据就能使用；在微软的 36 项基准上平均准确率最高，在 JevBench 的 27 个 API 产品中综合排名第 6。</li>
<li><strong>鲁棒性好</strong>：对同一请求做 8 种等价扰动，平均翻转率只有 1.3%，选项改写或重排时不翻转。在提示词和选项不断演进的生产环境中，这一点很重要。</li>
<li><strong>单次成本更低</strong>：标价与 Jev 相同，按 JevBench 的实测，每千次决策的成本还要低约 40%。</li>
<li><strong>接口兼容，迁移成本低</strong>：请求格式与 Jev 的 System One 风格接口几乎一致，已有的问题设计和分流逻辑可以直接复用（阈值需要重新标定）。</li>
<li><strong>有具体的落地参考</strong>：Xbox、Copilot、事故响应、Microsoft Discovery 等内部试点给出了可以借鉴的用法，虽然这些同样是自测数据。</li>
</ol>
<h3 id="劣势">劣势</h3>
<ol>
<li><strong>以自测数据为主，第三方结果不如官方亮眼</strong>：在 JevBench 上，它的智能分和校准分都低于 Jev，在 API 榜单上综合排名第 6。</li>
<li><strong>校准不是最强</strong>：无论官方数据还是第三方数据，它的校准都落后于 Jev，上线前一定要用自己的数据标定阈值。</li>
<li><strong>上下文只有 32K</strong>：比 Jev 的 64K 短，长邮件串、长合同或 Agent 的长执行轨迹需要先截断、摘要或分块。</li>
<li><strong>权重不开放，也不能微调</strong>：虽然底座是开源的 Qwen3.5-9B，但模型本身只能以托管 API 的形式使用，无法本地或离线部署；目前文档只介绍了通过 state、instructions 和选项来调优，没有提供基于自有数据的微调。</li>
<li><strong>版本和底座都可能变化</strong>：目前只有版本 <code>1</code>，而微软已经表示会把它迁移到 MAI 或 OpenAI 的底座上。底座一换，概率分布很可能随之变化，调好的阈值需要重新标定。</li>
<li><strong>没有解释，且对措辞敏感</strong>：模型只返回数字，不给理由。官方文档也提醒：问题和选项的措辞、顺序会影响分数；校准在熟悉的任务类型上最可靠；模型的知识可能已经过时；作为安全过滤器时，它可能漏掉隐蔽的有害内容，也可能误拦正常内容。</li>
<li><strong>跨境与数据驻留</strong>：中国大陆的团队目前需要通过 Azure 全球版的 Foundry 调用它，要同时考虑跨境网络延迟和数据出境的合规要求；<code>GlobalStandard</code> 部署可能在任意区域处理推理请求，对数据驻留敏感的场景应选择 <code>DataZoneStandard</code>。</li>
</ol>
<h2 id="与-jevlaya-相比怎么选">与 Jev、Laya 相比，怎么选</h2>
<h3 id="1-场景对照表">1. 场景对照表</h3>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>推荐</th>
					<th>理由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>已经在用 Azure，需要统一计费、Entra ID 认证和企业合规</td>
					<td>Microsoft-Decision-1</td>
					<td>不引入新的供应商，和现有的 Azure 资源统一管理</td>
			</tr>
			<tr>
					<td>Agent 的多步控制（继续、重试、交接）、AI 评审、电脑操作中的下一步选择</td>
					<td>Microsoft-Decision-1</td>
					<td>同区域延迟低，官方明确支持评分细则类的问题</td>
			</tr>
			<tr>
					<td>云端的海量分类和打标，追求单次成本</td>
					<td>Microsoft-Decision-1</td>
					<td>单价相同，实测单次成本更低</td>
			</tr>
			<tr>
					<td>对置信度校准要求最高的阈值分流</td>
					<td>Jev</td>
					<td>官方和第三方数据中，校准都领先</td>
			</tr>
			<tr>
					<td>长文本判断（32K～64K token）</td>
					<td>Jev</td>
					<td>64K 上下文</td>
			</tr>
			<tr>
					<td>细粒度、选项很多的分类（几十到上百个选项）</td>
					<td>Jev</td>
					<td>最多支持 255 个选项，在 Banking77 这类细粒度意图分类上表现好</td>
			</tr>
			<tr>
					<td>数据不能离开自己的环境，或者需要离线运行</td>
					<td>Laya</td>
					<td>三者中唯一可以本地部署的选项</td>
			</tr>
			<tr>
					<td>每秒几十次以上的实时决策、端侧应用</td>
					<td>Laya</td>
					<td>本地推理，没有网络往返</td>
			</tr>
			<tr>
					<td>有大量标注数据的垂直任务</td>
					<td>Laya（微调后）</td>
					<td>特化之后更快、更准、更便宜</td>
			</tr>
			<tr>
					<td>中文等非英语场景</td>
					<td>三者都需要自测</td>
					<td>Jev 以英语为主；Laya 要使用多语言检查点；Microsoft-Decision-1 没有公布支持语言的细节</td>
			</tr>
	</tbody>
</table>
<h3 id="2-选型时要回答的几个问题">2. 选型时要回答的几个问题</h3>
<p>可以按下面的顺序依次回答：</p>
<pre tabindex="0"><code>1. 数据能否离开自己的环境（以及境内）？
   └─ 不能 → Laya（硬约束，其他因素都要让路）
2. 是否已经在用 Azure，需要统一账单、Entra ID、RBAC 和数据区域部署？
   └─ 是 → 优先评估 Microsoft-Decision-1
3. 是否高度依赖置信度阈值，或者单次输入经常超过 32K token？
   └─ 是 → 优先评估 Jev
4. 是否有标注数据，并且需要每秒几十次以上的决策？
   └─ 是 → 微调 Laya，本地部署
5. 以上都不是硬约束？
   └─ 用同一组题目同时测试 Microsoft-Decision-1 和 Jev，
      按准确率、校准、延迟和单次成本综合选择
</code></pre><h3 id="3-组合使用不必三选一">3. 组合使用：不必三选一</h3>
<p>由于三者的接口几乎一致，组合使用的成本很低：</p>
<ol>
<li><strong>多供应商容灾</strong>：把 Microsoft-Decision-1 和 Jev 配置成互为备份的两个云端后端，一方限流或故障时自动切换。两者的概率分布不同，阈值需要分别标定。</li>
<li><strong>影子测试</strong>：生产流量走一个后端，同时把请求异步发给另一个后端，积累两者结论不一致的样本，再交给人工或大模型裁决，为后续的选型和阈值调整提供依据。</li>
<li><strong>分层级联</strong>：本地的 Laya 先处理高频、短文本、高置信度的请求；低置信度或长文本请求升级给 Microsoft-Decision-1 或 Jev；仍然拿不准的，再交给推理型大模型或人工处理。</li>
<li><strong>按数据敏感度路由</strong>：涉及敏感数据的请求只走本地的 Laya，其余请求走云端。</li>
</ol>
<h2 id="代码示例同一组问题三种后端">代码示例：同一组问题，三种后端</h2>
<h3 id="1-部署-microsoft-decision-1">1. 部署 Microsoft-Decision-1</h3>
<p>可以在 Foundry 门户的模型目录中搜索 Microsoft-Decision-1 并一键部署，也可以使用 Azure CLI：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>az cognitiveservices account deployment create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --name &lt;ACCOUNT_NAME&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --resource-group &lt;RESOURCE_GROUP&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --deployment-name decision-1 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --model-name <span style="color:#e6db74">&#34;Microsoft-Decision-1&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --model-format Microsoft <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --model-version <span style="color:#e6db74">&#34;1&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --sku-name GlobalStandard <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --sku-capacity <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>如果需要把推理限制在某个数据区域内，可以把 <code>--sku-name</code> 换成 <code>DataZoneStandard</code>（需要所在区域支持）。</p>
<h3 id="2-启动本地的-laya-服务">2. 启动本地的 Laya 服务</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install <span style="color:#e6db74">&#34;laya[serve]&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 只监听本机；如需对外提供服务，请设置 LAYA_API_KEY 并放在网关后面</span>
</span></span><span style="display:flex;"><span>LAYA_HOST<span style="color:#f92672">=</span>127.0.0.1 LAYA_DEVICE<span style="color:#f92672">=</span>cuda LAYA_PRELOAD<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> laya-serve
</span></span></code></pre></div><h3 id="3-用同一组问题调用三个后端">3. 用同一组问题调用三个后端</h3>
<p>下面的示例使用 <code>requests</code> 和 <code>azure-identity</code>（<code>pip install requests azure-identity</code>）。注意 <code>team</code> 问题里加了一个 <code>cannot_tell</code> 选项，这是微软文档推荐的做法：当模型不应该被迫做出选择时，给它一个“无法判断”的出口。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> requests
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> azure.identity <span style="color:#f92672">import</span> DefaultAzureCredential
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;state&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Checkout is down again. This is my third request.&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;customer_contact_count&#34;</span>: <span style="color:#ae81ff">3</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;questions&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;team&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;choice&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;Which team should handle this request?&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;criteria&#34;</span>: {
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;billing&#34;</span>: <span style="color:#e6db74">&#34;Charges, invoices, and refunds&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;engineering&#34;</span>: <span style="color:#e6db74">&#34;Bugs, errors, and outages&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;support&#34;</span>: <span style="color:#e6db74">&#34;How-to and account questions&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;cannot_tell&#34;</span>: <span style="color:#e6db74">&#34;The message does not say enough to decide&#34;</span>,
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;severity&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;score&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;How severe is this issue?&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;criteria&#34;</span>: [<span style="color:#e6db74">&#34;Cosmetic&#34;</span>, <span style="color:#e6db74">&#34;Minor&#34;</span>, <span style="color:#e6db74">&#34;Major&#34;</span>, <span style="color:#e6db74">&#34;Critical&#34;</span>],
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;repeat_contact&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;noul&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;Has the customer contacted support before?&#34;</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">decide</span>(url: str, headers: dict, model: str <span style="color:#f92672">|</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>) <span style="color:#f92672">-&gt;</span> dict:
</span></span><span style="display:flex;"><span>    body <span style="color:#f92672">=</span> {<span style="color:#f92672">**</span>payload, <span style="color:#e6db74">&#34;model&#34;</span>: model} <span style="color:#66d9ef">if</span> model <span style="color:#66d9ef">else</span> payload
</span></span><span style="display:flex;"><span>    resp <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(url, headers<span style="color:#f92672">=</span>headers, json<span style="color:#f92672">=</span>body, timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>)
</span></span><span style="display:flex;"><span>    resp<span style="color:#f92672">.</span>raise_for_status()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> resp<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">&#34;answers&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 1) Microsoft-Decision-1（Foundry）：model 填部署名称，推荐使用 Entra ID 认证</span>
</span></span><span style="display:flex;"><span>token <span style="color:#f92672">=</span> DefaultAzureCredential()<span style="color:#f92672">.</span>get_token(<span style="color:#e6db74">&#34;https://cognitiveservices.azure.com/.default&#34;</span>)<span style="color:#f92672">.</span>token
</span></span><span style="display:flex;"><span>answers <span style="color:#f92672">=</span> decide(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#39;AZURE_ENDPOINT&#39;</span>]<span style="color:#f92672">.</span>rstrip(<span style="color:#e6db74">&#39;/&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">/providers/microsoft/v1/systemone&#34;</span>,
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;Authorization&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Bearer </span><span style="color:#e6db74">{</span>token<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>},
</span></span><span style="display:flex;"><span>    os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#34;DEPLOYMENT_NAME&#34;</span>],
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;decision-1:&#34;</span>, answers[<span style="color:#e6db74">&#34;team&#34;</span>][<span style="color:#e6db74">&#34;choice&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2) Jev（TypeSafe）：固定版本号，避免别名升级后调好的阈值失效</span>
</span></span><span style="display:flex;"><span>answers <span style="color:#f92672">=</span> decide(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;https://api.typesafe.ai/v1/systemone&#34;</span>,
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;Authorization&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Bearer </span><span style="color:#e6db74">{</span>os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#39;TYPESAFE_API_KEY&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>},
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;jev-1.13.0&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;jev:&#34;</span>, answers[<span style="color:#e6db74">&#34;team&#34;</span>][<span style="color:#e6db74">&#34;choice&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3) Laya（本地 laya-serve）：省略 model，由 Router 按语言自动选择检查点</span>
</span></span><span style="display:flex;"><span>answers <span style="color:#f92672">=</span> decide(<span style="color:#e6db74">&#34;http://127.0.0.1:8000/v1/systemone&#34;</span>, {})
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;laya:&#34;</span>, answers[<span style="color:#e6db74">&#34;team&#34;</span>][<span style="color:#e6db74">&#34;choice&#34;</span>])
</span></span></code></pre></div><p>如果使用 API Key 调用 Microsoft-Decision-1，把请求头换成 <code>{&quot;api-key&quot;: os.environ[&quot;AZURE_API_KEY&quot;]}</code> 即可。</p>
<p>三个后端返回的 <code>answers</code> 都以问题名为键，但有几个细节需要留意：</p>
<ul>
<li>Jev 和 Laya 的 <code>choice</code> 结果都包含 <code>choice</code>、<code>probabilities</code> 和 <code>confidence</code>，不过 Laya 的 <code>confidence</code> 计算方式和 Jev 不同，所以它额外提供了一个按 Jev 公式计算的 <code>x_jev_confidence</code>，方便沿用 Jev 上调好的阈值；</li>
<li>Microsoft-Decision-1 的文档说明会返回选中项和每个选项的概率，接入时建议先打印一次完整的响应，确认字段名之后再编写阈值逻辑；</li>
<li>Microsoft-Decision-1 的 <code>score</code> 返回的是各等级下标的概率加权平均值（四级量表的取值范围是 0～3，可能落在两个等级之间），文档建议把它用于相对排序和阈值判断，而不是当作绝对的校准评分。</li>
</ul>
<h2 id="落地注意事项">落地注意事项</h2>
<p>Microsoft-Decision-1 的官方文档给出了不少实用的建议，它们同样适用于 Jev 和 Laya：</p>
<ol>
<li><strong>先用代表性数据验证</strong>：用自己的标注样本测试准确率和延迟，再决定是否上线。</li>
<li><strong>按误判代价设置阈值</strong>：根据误报和漏报的代价分别设置置信度阈值；不应该强行选择时，加一个“无法判断”的选项。</li>
<li><strong>使用中性措辞，随机化选项顺序</strong>：测试改变选项顺序是否会影响结果。</li>
<li><strong>用 Coding Agent 迭代问题设计</strong>：让 Coding Agent 生成多组候选的 state、instructions 和选项，在标注集上评估、对比，人工审核之后再上线。</li>
<li><strong>涉及个人的重大决策必须有人参与</strong>：在信贷、招聘、住房、医疗、法律等场景中，只能把模型作为决策辅助，并告知受影响的用户有 AI 参与了决策；不要使用不必要的敏感属性，并持续监控不同群体之间的结果差异。</li>
<li><strong>阈值不能跨后端照搬</strong>：三个模型的概率分布不同，置信度的计算方式也可能不同，切换或混用后端时要分别标定。</li>
<li><strong>固定版本，持续监控</strong>：固定部署的模型版本，升级（包括底座迁移）之后重新标定阈值，并持续监控概率分布和校准漂移。</li>
</ol>
<h2 id="总结">总结</h2>
<p>用一句话概括三者：</p>

<blockquote>
  <p><strong>Microsoft-Decision-1 是“Azure 里的企业级决策组件”，Jev 是“开箱即用的云端通才”，Laya 是“可以特化的本地专才”。</strong></p>

</blockquote>
<p><img alt="三者的不同定位" loading="lazy" src="/zh/blog/2026/microsoft-decision-1/6-three-positions.png"></p>
<ul>
<li>如果你已经在用 Azure，需要统一的计费、身份认证和合规体系，或者想给 Agent 加一道低延迟的“决策闸门”，Microsoft-Decision-1 是最省事的选择；</li>
<li>如果你最看重置信度校准和长上下文，Jev 目前仍然更稳；</li>
<li>如果数据不能离开本地，或者需要毫秒级、海量的决策，并且有能力准备标注数据，Laya 依然不可替代。</li>
</ul>
<p>从更大的视角看，Microsoft-Decision-1 的意义不只是“又多了一个决策模型”。微软带着开源底座、与 Jev 相同的价格和几乎一致的接口入局，说明了两件事：一是 <code>state + questions</code> 风格的决策接口正在成为事实标准，用户的迁移成本越来越低；二是底座正在变成可替换的零件，竞争的焦点转向了后训练数据、评测体系、校准质量和分发渠道。对开发者来说这是好消息：<strong>把决策从大模型里拆出来，已经从创业公司和开源社区的尝试，变成了大厂也认可的工程实践。</strong></p>
<h2 id="参考资料">参考资料</h2>
<ul>
<li><a href="https://commandline.microsoft.com/microsoft-decision-1-model-foundry/">Microsoft Command Line：Introducing Microsoft-Decision-1, our model for fast decision-making</a></li>
<li><a href="https://learn.microsoft.com/en-us/azure/foundry/foundry-models/how-to/use-foundry-models-microsoft-decision">Microsoft Learn：Deploy and use Microsoft-Decision-1 in Microsoft Foundry</a></li>
<li><a href="https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/models-sold-directly-by-azure">Microsoft Learn：Foundry Models sold by Azure</a></li>
<li><a href="https://ai.azure.com/catalog/models/Microsoft-Decision-1">Microsoft Foundry 模型目录：Microsoft-Decision-1</a></li>
<li><a href="https://benchmarkheaven.com/jev-models/api">JevBench：Hosted decision API benchmark</a></li>
<li><a href="https://benchmarkheaven.com/jev-models">JevBench：Open-weights decision model leaderboard</a></li>
<li><a href="https://docs.typesafe.ai/models">TypeSafe 文档：Models</a></li>
<li><a href="https://github.com/NandhaKishorM/laya">Laya GitHub 仓库</a></li>
<li><a href="/zh/blog/2026/jev-vs-laya/">Jev vs Laya：闭源与开源 System One 决策模型的对比与选型</a></li>
</ul>
]]></content:encoded></item><item><title>使用 Azure 托管 PostgreSQL 降低自建数据库的安全风险</title><link>https://wilsonwu.me/zh/blog/2026/azure-managed-postgresql-vs-self-hosted-security/</link><pubDate>Fri, 14 Aug 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/azure-managed-postgresql-vs-self-hosted-security/</guid><description>&lt;h2 id="问题的背景"&gt;问题的背景&lt;/h2&gt;
&lt;p&gt;&lt;img alt="PostgreSQL 安全问题" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/azure-managed-postgresql-vs-self-hosted-security/1-overview.png"&gt;&lt;/p&gt;
&lt;p&gt;最近，一台部署在 Azure 上的 Linux 服务器突然出现 CPU 持续 100% 的情况。排查后发现，服务器上存在一个以 &lt;code&gt;postgres&lt;/code&gt; 用户身份运行的恶意程序，并通过 &lt;code&gt;cron&lt;/code&gt; 定时任务维持运行。&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="问题的背景">问题的背景</h2>
<p><img alt="PostgreSQL 安全问题" loading="lazy" src="/zh/blog/2026/azure-managed-postgresql-vs-self-hosted-security/1-overview.png"></p>
<p>最近，一台部署在 Azure 上的 Linux 服务器突然出现 CPU 持续 100% 的情况。排查后发现，服务器上存在一个以 <code>postgres</code> 用户身份运行的恶意程序，并通过 <code>cron</code> 定时任务维持运行。</p>
<p>这台服务器上的 PostgreSQL：</p>
<ul>
<li>没有通过 Azure NSG 向公网开放 <code>5432</code>；</li>
<li>没有被当前业务使用；</li>
<li>业务实际使用的是本机 MySQL；</li>
<li>但 PostgreSQL 服务仍然安装并运行；</li>
<li>系统中保留了 <code>postgres</code> 用户、主目录、cron 和历史脚本。</li>
</ul>
<p>这次事件说明：</p>

<blockquote>
  <p>PostgreSQL 没有向公网开放，并不代表服务器上与 PostgreSQL 相关的组件完全没有风险。</p>

</blockquote>
<p>需要特别说明的是，目前的证据只能证明恶意程序利用了 <code>postgres</code> 操作系统用户及其目录进行持久化，<strong>不能证明攻击者一定是通过 PostgreSQL 的 5432 端口进入服务器的</strong>。初始入口也可能是历史配置、SSH、Web 应用、容器、旧镜像或其他漏洞。</p>
<p>如果生产环境确实需要 PostgreSQL，但没有必须自建数据库服务器的特殊要求，建议优先使用：</p>

<blockquote>
  <p><strong>Azure Database for PostgreSQL，并通过私有网络访问，关闭公共网络入口。</strong></p>

</blockquote>
<p>这样可以减少自建数据库所带来的操作系统用户、SSH、cron、systemd、补丁、备份和主机入侵等风险。</p>
<h2 id="发现问题服务器-cpu-持续占满">发现问题：服务器 CPU 持续占满</h2>
<p>最初是在服务器上执行 <code>htop</code> 时发现异常。</p>
<p><img alt="服务器 htop 中 CPU 占用异常" loading="lazy" src="/zh/blog/2026/azure-managed-postgresql-vs-self-hosted-security/2-htop.png"></p>
<p>从截图中可以看到：</p>
<ul>
<li>服务器共有 4 个 CPU 核心；</li>
<li>4 个核心全部达到 <code>100%</code>；</li>
<li>Load Average 长期维持在 4 左右；</li>
<li>多个进程以 <code>postgres</code> 用户运行；</li>
<li>进程名称为随机字符串 <code>dzZ7yVrxv7</code>；</li>
<li>单个进程的 CPU 使用率超过 <code>200%</code>；</li>
<li>进程占用了大量内存。</li>
</ul>
<p>进一步执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ps -eo user,pid,ppid,lstart,etime,%cpu,%mem,stat,comm,args <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --sort<span style="color:#f92672">=</span>-%cpu | head -20
</span></span></code></pre></div><p>其中一个主要进程如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>USER       PID   PPID  %CPU  %MEM  STAT  COMMAND
</span></span><span style="display:flex;"><span>postgres   9918     1   377  29.8  Ssl   dzZ7yVrxv7
</span></span></code></pre></div><p>这不是正常的 PostgreSQL 进程。</p>
<p>正常情况下，PostgreSQL 进程名称通常类似：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>postgres
</span></span><span style="display:flex;"><span>postgres: checkpointer
</span></span><span style="display:flex;"><span>postgres: background writer
</span></span><span style="display:flex;"><span>postgres: walwriter
</span></span><span style="display:flex;"><span>postgres: autovacuum launcher
</span></span></code></pre></div><p>而本次进程具有以下异常特征：</p>
<ul>
<li>名称为随机字符串；</li>
<li>以 <code>postgres</code> 用户运行；</li>
<li>CPU 使用率接近 4 个核心；</li>
<li>父进程已经变成 PID 1；</li>
<li>内存占用异常高。</li>
</ul>
<p>因此，初步判断这不是正常数据库负载，而是疑似挖矿程序或其他恶意程序。</p>
<h2 id="分析问题确认恶意程序来源">分析问题：确认恶意程序来源</h2>
<h3 id="1-检查进程真实文件">1. 检查进程真实文件</h3>
<p>Linux 进程名称可以被修改或伪装，所以不能只看 <code>COMMAND</code>，需要检查 <code>/proc/&lt;PID&gt;/exe</code>。</p>
<p>执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">echo &#34;EXE:&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ls -l /proc/9918/exe
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">echo &#34;CWD:&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ls -l /proc/9918/cwd
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">echo &#34;CMDLINE:&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">tr &#34;\0&#34; &#34; &#34; &lt; /proc/9918/cmdline
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">echo
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">echo &#34;CGROUP:&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">cat /proc/9918/cgroup
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span>
</span></span></code></pre></div><p>输出如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>EXE:
</span></span><span style="display:flex;"><span>/proc/9918/exe -&gt; /dev/shm/gJppJl6TbG (deleted)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>CWD:
</span></span><span style="display:flex;"><span>/proc/9918/cwd -&gt; /var/lib/postgresql
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>CMDLINE:
</span></span><span style="display:flex;"><span>dzZ7yVrxv7
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>CGROUP:
</span></span><span style="display:flex;"><span>0::/system.slice/cron.service
</span></span></code></pre></div><p>这几项信息非常关键。</p>
<h3 id="可执行文件位于-devshm">可执行文件位于 <code>/dev/shm</code></h3>
<p>真实程序路径为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/dev/shm/gJppJl6TbG
</span></span></code></pre></div><p><code>/dev/shm</code> 是基于内存的临时文件系统。恶意程序经常将文件下载到这里，再从内存文件系统中执行，以减少在磁盘上的痕迹。</p>
<h3 id="原文件已经被删除">原文件已经被删除</h3>
<p>路径末尾显示：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>(deleted)
</span></span></code></pre></div><p>说明程序启动后，磁盘上的原文件已被删除。但由于进程仍然持有该文件，程序依然可以继续运行。</p>
<p>这种“启动后删除自身”的行为是常见的恶意程序隐藏手段。</p>
<h3 id="工作目录属于-postgresql-用户">工作目录属于 PostgreSQL 用户</h3>
<p>进程工作目录是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/var/lib/postgresql
</span></span></code></pre></div><p>这说明恶意程序与 <code>postgres</code> 用户的主目录有关。</p>
<h3 id="进程由-cron-启动">进程由 cron 启动</h3>
<p>cgroup 显示：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/system.slice/cron.service
</span></span></code></pre></div><p>这说明程序是通过定时任务启动的。</p>
<h3 id="2-从运行中的进程保存样本">2. 从运行中的进程保存样本</h3>
<p>因为原始文件已经被删除，如果立即结束进程，可能无法再提取样本。因此先从 <code>/proc</code> 保存程序：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo mkdir -p /root/incident
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">700</span> /root/incident
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sudo cp -L /proc/9918/exe <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/suspect-9918.bin
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">600</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/suspect-9918.bin
</span></span></code></pre></div><p>检查文件类型：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo file /root/incident/suspect-9918.bin
</span></span></code></pre></div><p>结果如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ELF 64-bit LSB shared object, x86-64,
</span></span><span style="display:flex;"><span>statically linked, no section header
</span></span></code></pre></div><p>计算 SHA-256：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo sha256sum /root/incident/suspect-9918.bin
</span></span></code></pre></div><p>得到：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>d32f9eef63c496a146a6a811677dd27e337db993d6f1a4b1af145c3135d391b3
</span></span></code></pre></div><p>该文件具有以下特征：</p>
<ul>
<li>随机文件名；</li>
<li>位于 <code>/dev/shm</code>；</li>
<li>静态链接；</li>
<li>没有节区表；</li>
<li>启动后删除原文件；</li>
<li>持续占用全部 CPU。</li>
</ul>
<p>这些行为进一步证明它不是正常的 PostgreSQL 组件。</p>
<h3 id="3-检查-postgresql-用户的定时任务">3. 检查 PostgreSQL 用户的定时任务</h3>
<p>由于进程是从 <code>cron.service</code> 启动的，继续检查 <code>postgres</code> 用户的 crontab：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo crontab -u postgres -l
</span></span></code></pre></div><p>发现如下任务：</p>
<pre tabindex="0"><code class="language-cron" data-lang="cron">24 * * * * /var/lib/postgresql/.dbus-XGodM5jOyL
</code></pre><p>该任务会在每小时第 24 分钟执行一次隐藏脚本：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/var/lib/postgresql/.dbus-XGodM5jOyL
</span></span></code></pre></div><p>文件名使用了以点开头的隐藏形式，并模仿了 D-Bus 的命名方式，很容易被误认为系统文件。</p>
<p>检查 cron 日志：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo journalctl -u cron.service <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --since <span style="color:#e6db74">&#34;2026-08-15 07:15:00&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --until <span style="color:#e6db74">&#34;2026-08-15 09:15:00&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-pager
</span></span></code></pre></div><p>日志中可以看到：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Aug 15 07:24:01 CRON: (postgres) CMD (/var/lib/postgresql/.dbus-XGodM5jOyL)
</span></span><span style="display:flex;"><span>Aug 15 08:24:01 CRON: (postgres) CMD (/var/lib/postgresql/.dbus-XGodM5jOyL)
</span></span></code></pre></div><p>这证明恶意脚本至少在 <code>07:24</code> 和 <code>08:24</code> 被执行过。</p>
<h3 id="4-分析恶意启动脚本">4. 分析恶意启动脚本</h3>
<p>检查文件：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo file /var/lib/postgresql/.dbus-XGodM5jOyL
</span></span><span style="display:flex;"><span>sudo stat /var/lib/postgresql/.dbus-XGodM5jOyL
</span></span><span style="display:flex;"><span>sudo sha256sum /var/lib/postgresql/.dbus-XGodM5jOyL
</span></span></code></pre></div><p>结果显示它是一个 Bash 脚本：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Bourne-Again shell script, ASCII text executable,
</span></span><span style="display:flex;"><span>with very long lines
</span></span></code></pre></div><p>对应 SHA-256：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>36b1dcf43876f55aae2b8be7dada848b41f60ec61cd4ca7ab2a1278193f286f0
</span></span></code></pre></div><p>这份脚本负责下载、启动或恢复恶意程序，也是恶意进程能够周期性出现的主要持久化机制。</p>
<p>如果只结束高 CPU 进程，而不清理这条 cron，恶意程序会在下一个小时的第 24 分钟再次出现。</p>
<h3 id="5-在用户目录中发现第二个恶意下载器">5. 在用户目录中发现第二个恶意下载器</h3>
<p>停止并卸载 PostgreSQL 后，我继续检查 <code>/var/lib/postgresql</code> 的残留文件：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo find /var/lib/postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -xdev -maxdepth <span style="color:#ae81ff">10</span> -ls
</span></span></code></pre></div><p>发现了另一个可疑文件：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/var/lib/postgresql/.config/systemd/user/
</span></span><span style="display:flex;"><span>└── systemd-tmpfiles-cleanup/
</span></span><span style="display:flex;"><span>    └── systemd-tmpfiles-cleanup-TNWow7.sh
</span></span></code></pre></div><p>这个文件使用了：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>systemd-tmpfiles-cleanup
</span></span></code></pre></div><p>作为目录名，试图伪装成正常的 systemd 临时文件清理组件。</p>
<p>检查文件类型和哈希：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo file <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql/.config/systemd/user/systemd-tmpfiles-cleanup/systemd-tmpfiles-cleanup-TNWow7.sh
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sudo sha256sum <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql/.config/systemd/user/systemd-tmpfiles-cleanup/systemd-tmpfiles-cleanup-TNWow7.sh
</span></span></code></pre></div><p>结果为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Bourne-Again shell script, ASCII text executable,
</span></span><span style="display:flex;"><span>with very long lines
</span></span></code></pre></div><p>SHA-256：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>9962de5e0c68ea6e85879c5f62f2d0806360dbe186dc15e1d59abf8e4209de11
</span></span></code></pre></div><p>脚本内容经过 Base64 编码，末尾直接执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>base64 -d | bash
</span></span></code></pre></div><p>解码后的逻辑显示，它具备以下行为：</p>
<ul>
<li>通过 Tor、SOCKS5 或 Tor2web 连接外部地址；</li>
<li>获取服务器公网 IP；</li>
<li>收集当前用户名、主机名和内核版本；</li>
<li>读取机器 ID；</li>
<li>从外部下载其他程序；</li>
<li>在 <code>/dev/shm</code>、<code>/tmp</code>、<code>/var/tmp</code> 等目录测试和执行文件；</li>
<li>启动程序后删除临时文件。</li>
</ul>
<p>这已经不只是一个单纯的高 CPU 进程，而是一套包含下载、执行、隐藏和持久化能力的恶意程序。</p>
<h2 id="为什么-postgresql-没有开放公网端口仍然存在风险">为什么 PostgreSQL 没有开放公网端口，仍然存在风险？</h2>
<p>Azure 网络安全组当时只允许公网访问：</p>
<ul>
<li>TCP 22；</li>
<li>TCP 80；</li>
<li>TCP 443。</li>
</ul>
<p>并没有允许公网直接访问 PostgreSQL 的 <code>5432</code> 端口。</p>
<p>但检查服务器本机监听状态时发现：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ss -lntp | grep <span style="color:#e6db74">&#39;:5432&#39;</span>
</span></span></code></pre></div><p>输出为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>LISTEN 0 244 0.0.0.0:5432 0.0.0.0:*
</span></span><span style="display:flex;"><span>LISTEN 0 244 [::]:5432    [::]:*
</span></span></code></pre></div><p>这说明 PostgreSQL 在操作系统层面监听了所有 IPv4 和 IPv6 网卡。</p>
<p>虽然 Azure NSG 没有向公网放行 <code>5432</code>，但以下风险依然存在。</p>
<h3 id="1-同一虚拟网络中的主机仍可能访问">1. 同一虚拟网络中的主机仍可能访问</h3>
<p>Azure NSG 默认存在 <code>AllowVnetInBound</code> 规则，同一虚拟网络中的其他服务器可能访问该端口。</p>
<p>如果同一 VNet 内的其他服务器被入侵，攻击者就可能通过内网继续攻击数据库服务器。</p>
<h3 id="2-本机应用或容器仍可能访问">2. 本机应用或容器仍可能访问</h3>
<p>即使公网无法连接，服务器本机运行的以下组件仍然可能访问 PostgreSQL：</p>
<ul>
<li>Web 应用；</li>
<li>Docker 容器；</li>
<li>CI/CD Agent；</li>
<li>其他被攻陷的服务；</li>
<li>本机恶意程序。</li>
</ul>
<p>网络安全组无法阻止服务器本机进程访问本机数据库。</p>
<h3 id="3-风险不只来自数据库端口">3. 风险不只来自数据库端口</h3>
<p>本次恶意程序的实际持久化位置是：</p>
<ul>
<li><code>postgres</code> 用户的 crontab；</li>
<li><code>/var/lib/postgresql</code> 用户目录；</li>
<li>用户级 systemd 配置目录；</li>
<li><code>/dev/shm</code> 临时执行目录。</li>
</ul>
<p>即使 PostgreSQL 服务停止，只要这些用户、目录和定时任务仍然存在，恶意程序就可能继续运行。</p>
<p>所以，数据库的安全边界不只是 <code>5432</code> 端口，还包括：</p>
<ul>
<li>操作系统账户；</li>
<li>数据目录；</li>
<li>用户主目录；</li>
<li>cron；</li>
<li>systemd；</li>
<li>SSH；</li>
<li>软件包；</li>
<li>配置文件；</li>
<li>历史镜像和备份。</li>
</ul>
<h3 id="4-未使用的软件往往更容易被忽略">4. 未使用的软件往往更容易被忽略</h3>
<p>这台服务器的业务实际使用 MySQL，PostgreSQL 并没有业务用途。</p>
<p>由于“不使用”，它反而容易长期处于以下状态：</p>
<ul>
<li>没有人检查配置；</li>
<li>没有人检查系统用户；</li>
<li>没有人检查数据库日志；</li>
<li>没有人检查用户的 cron；</li>
<li>没有人检查相关目录；</li>
<li>没有人确认是否仍然需要；</li>
<li>没有人关注其补丁和版本。</li>
</ul>
<p>因此：</p>

<blockquote>
  <p>未使用但仍然安装和运行的软件，并不是备用能力，而是未被管理的攻击面。</p>

</blockquote>
<h2 id="解决问题清除恶意程序并卸载-postgresql">解决问题：清除恶意程序并卸载 PostgreSQL</h2>
<h3 id="1-暂停恶意进程">1. 暂停恶意进程</h3>
<p>取证完成后，先暂停进程：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo kill -STOP <span style="color:#ae81ff">9918</span>
</span></span></code></pre></div><p>检查进程状态：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ps -p <span style="color:#ae81ff">9918</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o user,pid,ppid,%cpu,%mem,stat,comm,args
</span></span></code></pre></div><p>状态中出现 <code>T</code>，说明进程已暂停。</p>
<p>暂停的好处是：</p>
<ul>
<li>立即停止继续占用 CPU；</li>
<li>暂时停止潜在网络行为；</li>
<li>保留进程，便于继续调查；</li>
<li>避免在完成取证前丢失内存中的样本。</li>
</ul>
<h3 id="2-备份并删除恶意-cron">2. 备份并删除恶意 cron</h3>
<p>先保存 crontab：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo crontab -u postgres -l <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  &gt; /root/incident/postgres-crontab.txt
</span></span></code></pre></div><p>然后删除：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo crontab -u postgres -r
</span></span></code></pre></div><p>确认已删除：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo crontab -u postgres -l
</span></span></code></pre></div><p>预期输出：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>no crontab for postgres
</span></span></code></pre></div><h3 id="3-隔离恶意启动脚本">3. 隔离恶意启动脚本</h3>
<p>先备份脚本：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo cp -a <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql/.dbus-XGodM5jOyL <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/cron-payload
</span></span></code></pre></div><p>再从原位置移走：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo mv <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql/.dbus-XGodM5jOyL <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/original-dbus-XGodM5jOyL
</span></span></code></pre></div><p>限制证据文件权限：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo chown root:root <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/original-dbus-XGodM5jOyL <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/cron-payload
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">600</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/original-dbus-XGodM5jOyL <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/cron-payload
</span></span></code></pre></div><h3 id="4-结束恶意进程">4. 结束恶意进程</h3>
<p>确认恶意 cron 和启动脚本已隔离后，结束进程：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo kill -KILL <span style="color:#ae81ff">9918</span>
</span></span></code></pre></div><p>检查进程是否消失：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ps -p <span style="color:#ae81ff">9918</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o user,pid,ppid,%cpu,%mem,stat,comm,args
</span></span></code></pre></div><p>随后查看系统负载：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>uptime
</span></span></code></pre></div><p>清理后的负载变为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>load average: 0.36, 2.71, 3.68
</span></span></code></pre></div><p>1 分钟负载已经从 4 以上下降到 <code>0.36</code>。</p>
<p>5 分钟和 15 分钟负载仍然较高是正常的，因为它们反映的是一段时间内的平均值，会逐渐下降。</p>
<h3 id="5-停止并禁用-postgresql">5. 停止并禁用 PostgreSQL</h3>
<p>因为业务并不使用 PostgreSQL，所以先停止并禁止开机启动：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl disable --now postgresql
</span></span></code></pre></div><p>确认服务状态：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl status postgresql --no-pager
</span></span></code></pre></div><p>确认端口不再监听：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ss -lntp | grep <span style="color:#e6db74">&#39;:5432&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;5432 端口未监听&#34;</span>
</span></span></code></pre></div><p>最终结果为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>5432 端口未监听
</span></span></code></pre></div><h3 id="6-卸载-postgresql">6. 卸载 PostgreSQL</h3>
<p>确认业务没有受到影响后，卸载 PostgreSQL 相关软件包：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get purge -y <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-14 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-client-14 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-client-common <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-common <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-contrib
</span></span></code></pre></div><p>需要注意：</p>

<blockquote>
  <p>卸载软件包不等于完成恶意程序清理。</p>

</blockquote>
<p>还应继续检查以下位置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/var/lib/postgresql
</span></span><span style="display:flex;"><span>/etc/postgresql
</span></span><span style="display:flex;"><span>/etc/postgresql-common
</span></span><span style="display:flex;"><span>/var/spool/cron
</span></span><span style="display:flex;"><span>/tmp
</span></span><span style="display:flex;"><span>/var/tmp
</span></span><span style="display:flex;"><span>/dev/shm
</span></span><span style="display:flex;"><span>/home/*/.config/systemd
</span></span></code></pre></div><p>本次发现的第二个恶意下载器，就是在卸载 PostgreSQL 后检查残留目录时找到的。</p>
<h3 id="7-隔离第二个恶意下载器">7. 隔离第二个恶意下载器</h3>
<p>将伪装成 systemd 清理任务的脚本移入证据目录：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo mv <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql/.config/systemd/user/systemd-tmpfiles-cleanup/systemd-tmpfiles-cleanup-TNWow7.sh <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/systemd-tmpfiles-cleanup-TNWow7.sh
</span></span></code></pre></div><p>限制权限：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo chown root:root <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/systemd-tmpfiles-cleanup-TNWow7.sh
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">600</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /root/incident/systemd-tmpfiles-cleanup-TNWow7.sh
</span></span></code></pre></div><p>然后搜索是否存在同类文件：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo grep -RIlE <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;TNWow7|XGodM5jOyL|tor2socks|tor2web|relay\.tor2socks&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /etc <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/spool/cron <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /tmp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/tmp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /dev/shm <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /home <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  2&gt;/dev/null
</span></span></code></pre></div><h2 id="azure-网络安全配置整改">Azure 网络安全配置整改</h2>
<h3 id="1-不需要删除不存在的-5432-入站规则">1. 不需要删除不存在的 5432 入站规则</h3>
<p>Azure NSG 中原本没有允许公网访问 <code>5432</code> 的规则，所以不需要专门删除 PostgreSQL 入站规则。</p>
<p>停止并卸载 PostgreSQL 后，本机也已经不再监听 <code>5432</code>。</p>
<p>但是，不能因为 NSG 没有开放 <code>5432</code> 就认为服务器已经安全。</p>
<h3 id="2-收紧-ssh-入站范围">2. 收紧 SSH 入站范围</h3>
<p>排查过程中，SSH 日志中出现了多个公网 IP 对 <code>postgres</code> 用户进行密码尝试：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Failed password for postgres from 172.83.83.194
</span></span><span style="display:flex;"><span>Failed password for postgres from 101.36.110.41
</span></span></code></pre></div><p>这些记录显示的是失败登录，不能说明对方已经通过 SSH 登录成功。但它们说明服务器的 SSH 正在被公网持续扫描。</p>
<p>原 Azure SSH 规则为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Source: Any
</span></span><span style="display:flex;"><span>Destination port: 22
</span></span><span style="display:flex;"><span>Action: Allow
</span></span></code></pre></div><p>建议改为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Source: 管理员固定公网 IP/32
</span></span><span style="display:flex;"><span>Destination port: 22
</span></span><span style="display:flex;"><span>Protocol: TCP
</span></span><span style="display:flex;"><span>Action: Allow
</span></span></code></pre></div><p>更安全的方式包括：</p>
<ul>
<li>Azure Bastion；</li>
<li>VPN；</li>
<li>Just-In-Time VM Access；</li>
<li>禁止 SSH 密码登录；</li>
<li>只使用 SSH Key；</li>
<li>禁止 root 远程登录；</li>
<li>配置失败登录告警。</li>
</ul>
<h3 id="3-限制服务器出站访问">3. 限制服务器出站访问</h3>
<p>恶意脚本需要连接外部地址下载程序，而当时 Azure NSG 允许任意出站访问。</p>
<p>如果业务条件允许，应限制服务器的出站范围：</p>
<ul>
<li>只允许必要的 DNS；</li>
<li>只允许必要的软件源；</li>
<li>只允许业务需要访问的 API；</li>
<li>使用 Azure Firewall 或集中式代理；</li>
<li>记录 DNS 和出站网络日志；</li>
<li>对 Tor、代理、矿池和异常域名建立告警。</li>
</ul>
<p>仅限制入站流量是不够的。恶意程序一旦进入服务器，通常还需要通过出站网络下载载荷、连接控制服务器或矿池。</p>
<h2 id="为什么建议使用-azure-托管-postgresql">为什么建议使用 Azure 托管 PostgreSQL？</h2>
<p>如果业务确实需要 PostgreSQL，建议优先使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Azure Database for PostgreSQL
</span></span></code></pre></div><p>而不是直接在 Azure VM 上安装 PostgreSQL。</p>
<h3 id="1-减少操作系统层面的攻击面">1. 减少操作系统层面的攻击面</h3>
<p>在 VM 上自建 PostgreSQL，需要自行维护：</p>
<ul>
<li>Linux 操作系统；</li>
<li>PostgreSQL 软件包；</li>
<li><code>postgres</code> 系统用户；</li>
<li>SSH；</li>
<li>cron；</li>
<li>systemd；</li>
<li>数据目录权限；</li>
<li>安全补丁；</li>
<li>版本升级；</li>
<li>网络防火墙；</li>
<li>日志；</li>
<li>备份；</li>
<li>高可用；</li>
<li>恶意进程检测。</li>
</ul>
<p>使用 Azure 托管 PostgreSQL 后，用户不会直接管理数据库所在主机的 root 权限，也不会在数据库主机上创建 cron、systemd 服务或运行 <code>/dev/shm</code> 中的程序。</p>
<p>这可以显著减少本次事件中出现的主机级风险。</p>
<h3 id="2-使用私有网络关闭公网访问">2. 使用私有网络，关闭公网访问</h3>
<p>建议采用以下网络架构：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Azure VM / App Service / AKS
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>      Private Network
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>Azure Database for PostgreSQL
</span></span></code></pre></div><p>数据库应配置为：</p>
<ul>
<li>禁止公共网络访问；</li>
<li>使用 Private Endpoint 或私有网络集成；</li>
<li>只允许应用所在子网访问；</li>
<li>不向公网开放数据库端口；</li>
<li>数据库管理也通过 VPN、Bastion 或受控网络完成。</li>
</ul>
<h3 id="3-自动备份和维护">3. 自动备份和维护</h3>
<p>Azure 托管 PostgreSQL 可以提供：</p>
<ul>
<li>自动备份；</li>
<li>时间点恢复；</li>
<li>高可用选项；</li>
<li>数据加密；</li>
<li>监控指标；</li>
<li>日志与告警；</li>
<li>平台维护；</li>
<li>安全更新；</li>
<li>版本支持。</li>
</ul>
<p>这些能力可以降低因长期无人维护而产生的风险。</p>
<h3 id="4-责任边界更清晰">4. 责任边界更清晰</h3>
<p>自建 PostgreSQL 经常出现这种情况：</p>

<blockquote>
  <p>为了测试临时安装，后来业务不用了，但服务、用户和目录一直留在服务器上。</p>

</blockquote>
<p>托管数据库的资源边界更加清晰：</p>
<ul>
<li>需要时创建；</li>
<li>通过私有网络连接；</li>
<li>不再使用时导出数据；</li>
<li>删除数据库实例；</li>
<li>删除访问权限和私有连接；</li>
<li>不会在业务 VM 上留下数据库系统用户和历史脚本。</li>
</ul>
<h3 id="5-托管服务并不等于绝对安全">5. 托管服务并不等于绝对安全</h3>
<p>Azure 托管 PostgreSQL 可以减少操作系统和基础设施维护风险，但仍需要正确管理：</p>
<ul>
<li>数据库账号；</li>
<li>强密码或身份认证；</li>
<li>最小权限；</li>
<li>网络访问范围；</li>
<li>SQL 注入；</li>
<li>连接字符串；</li>
<li>Azure Key Vault；</li>
<li>日志审计；</li>
<li>数据加密；</li>
<li>应用依赖；</li>
<li>凭据轮换。</li>
</ul>
<p>正确的理解是：</p>

<blockquote>
  <p>Azure 托管 PostgreSQL 缩小了需要自己负责的安全范围，但不会消除应用和账号层面的安全责任。</p>

</blockquote>
<h2 id="本次事件的-ioc">本次事件的 IOC</h2>
<p>以下指标可用于内部服务器排查。</p>
<h3 id="可疑进程及文件名">可疑进程及文件名</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>dzZ7yVrxv7
</span></span><span style="display:flex;"><span>gJppJl6TbG
</span></span><span style="display:flex;"><span>.dbus-XGodM5jOyL
</span></span><span style="display:flex;"><span>systemd-tmpfiles-cleanup-TNWow7.sh
</span></span></code></pre></div><h3 id="sha-256">SHA-256</h3>
<p>恶意 ELF 程序：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>d32f9eef63c496a146a6a811677dd27e337db993d6f1a4b1af145c3135d391b3
</span></span></code></pre></div><p>恶意 cron 启动脚本：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>36b1dcf43876f55aae2b8be7dada848b41f60ec61cd4ca7ab2a1278193f286f0
</span></span></code></pre></div><p>恶意下载器：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>9962de5e0c68ea6e85879c5f62f2d0806360dbe186dc15e1d59abf8e4209de11
</span></span></code></pre></div><h3 id="行为特征">行为特征</h3>
<ul>
<li>以 <code>postgres</code> 用户运行；</li>
<li>从 <code>/dev/shm</code> 执行随机名称文件；</li>
<li>启动后删除原文件；</li>
<li>利用用户 crontab 定时启动；</li>
<li>Base64 解码后直接交给 Bash 执行；</li>
<li>使用 Tor、SOCKS5 或 Tor2web；</li>
<li>收集公网 IP、用户名、主机名和机器 ID；</li>
<li>从外部下载其他程序；</li>
<li>在 <code>/tmp</code>、<code>/var/tmp</code> 和 <code>/dev/shm</code> 中执行程序；</li>
<li>持续占用全部 CPU；</li>
<li>伪装成 D-Bus 或 systemd 组件。</li>
</ul>
<h2 id="最终总结">最终总结</h2>
<p>本次事件从表面上看，是一次服务器 CPU 占用过高的问题。</p>
<p>但深入排查后发现，真正的问题包括：</p>
<ul>
<li>以 <code>postgres</code> 用户运行的恶意程序；</li>
<li>位于 <code>/dev/shm</code> 的随机 ELF 文件；</li>
<li><code>postgres</code> 用户的恶意 cron；</li>
<li><code>/var/lib/postgresql</code> 下的隐藏启动脚本；</li>
<li>伪装成 systemd 组件的恶意下载器；</li>
<li>可连接 Tor 和外部下载地址的恶意逻辑。</li>
</ul>
<p>最值得关注的是：</p>

<blockquote>
  <p>这台服务器没有通过 Azure NSG 向公网开放 PostgreSQL 的 5432 端口，而且业务也没有使用 PostgreSQL，但与 PostgreSQL 有关的系统用户、主目录、cron 和残留文件仍然构成了安全风险。</p>

</blockquote>
<p>当然，现有证据并不能证明初始入侵入口就是 PostgreSQL。恶意程序使用 <code>postgres</code> 用户运行，不等于攻击者一定通过 PostgreSQL 端口进入服务器。</p>
<p>但这次事件依然说明了几个重要原则。</p>
<h3 id="第一不使用的服务应立即停止和卸载">第一，不使用的服务应立即停止和卸载</h3>
<p>不要因为“没有向公网开放”就长期保留闲置服务。</p>
<p>不使用的软件应当：</p>
<ul>
<li>停止服务；</li>
<li>禁止开机启动；</li>
<li>卸载软件包；</li>
<li>检查系统用户；</li>
<li>检查 cron；</li>
<li>检查 systemd；</li>
<li>检查残留目录和脚本。</li>
</ul>
<h3 id="第二云安全组不是唯一安全边界">第二，云安全组不是唯一安全边界</h3>
<p>NSG 没有开放 <code>5432</code>，只能说明公网无法通过该规则直接连接数据库，不能防止：</p>
<ul>
<li>内网访问；</li>
<li>本机访问；</li>
<li>容器访问；</li>
<li>历史持久化；</li>
<li>用户级 cron；</li>
<li>本机恶意程序；</li>
<li>SSH 和应用漏洞。</li>
</ul>
<h3 id="第三生产-postgresql-优先使用-azure-托管服务">第三，生产 PostgreSQL 优先使用 Azure 托管服务</h3>
<p>如果没有必须自建数据库服务器的明确需求，建议使用：</p>

<blockquote>
  <p><strong>Azure Database for PostgreSQL + 私有网络 + 禁止公共访问。</strong></p>

</blockquote>
<p>这样可以减少数据库主机操作系统、SSH、系统用户、cron、systemd、补丁、备份和高可用等方面的维护风险。</p>
<h3 id="第四杀掉恶意进程不等于完成安全恢复">第四，杀掉恶意进程不等于完成安全恢复</h3>
<p>结束高 CPU 进程只解决了当前资源占用问题，还应继续：</p>
<ul>
<li>查找持久化；</li>
<li>调查入侵入口；</li>
<li>检查 SSH 成功登录记录；</li>
<li>检查 Web 应用和容器；</li>
<li>轮换服务器上的全部凭据；</li>
<li>检查同一 VNet 中的其他主机；</li>
<li>保存日志和磁盘快照；</li>
<li>必要时从可信镜像重建服务器。</li>
</ul>
<p>最终可以将这次事件概括为一句话：</p>

<blockquote>
  <p><strong>不使用的数据库不要留在服务器上；需要 PostgreSQL 时，优先使用 Azure 托管 PostgreSQL，并通过私有网络访问。</strong></p>

</blockquote>
]]></content:encoded></item><item><title>OpenWebUI 数据库迁移实践：从 VM 自建 PostgreSQL 平滑迁移到 Azure 托管 PostgreSQL</title><link>https://wilsonwu.me/zh/blog/2026/migrate-openwebui-to-azure-pg/</link><pubDate>Thu, 13 Aug 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/migrate-openwebui-to-azure-pg/</guid><description>&lt;p&gt;&lt;img alt="迁移" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/migrate-openwebui-to-azure-pg/1-overview.png"&gt;&lt;/p&gt;
&lt;p&gt;随着 OpenWebUI 中的用户、对话、文件和知识库数据不断增长，原先部署在 Azure VM 上的自建 PostgreSQL 带来了越来越多的运维工作，包括数据库补丁、备份、故障恢复、存储监控和安全维护。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="迁移" loading="lazy" src="/zh/blog/2026/migrate-openwebui-to-azure-pg/1-overview.png"></p>
<p>随着 OpenWebUI 中的用户、对话、文件和知识库数据不断增长，原先部署在 Azure VM 上的自建 PostgreSQL 带来了越来越多的运维工作，包括数据库补丁、备份、故障恢复、存储监控和安全维护。</p>
<p>为了降低维护成本并提高数据库可靠性，我将 <strong>OpenWebUI 的业务数据库</strong>从 VM 本地 PostgreSQL 迁移到了 <strong>Azure Database for PostgreSQL Flexible Server</strong>。</p>
<p>这次迁移采用了：</p>

<blockquote>
  <p>在线全量迁移 + 数据一致性验证 + 活跃对话增量同步 + 快速切换</p>

</blockquote>
<p>全量数据库导出和恢复期间，OpenWebUI 始终保持在线。最终切换仅涉及一次很短的 Docker 容器重建，用户侧基本无感。</p>

<blockquote>
  <p>本文中的服务器地址、数据库名称、用户名、密码和资源标识均已隐藏或替换为占位符。</p>

</blockquote>
<h2 id="迁移背景">迁移背景</h2>
<p>OpenWebUI 以 Docker 容器形式运行在 Azure VM 中，最初使用安装在同一台 VM 上的 PostgreSQL。</p>
<p>迁移前的架构如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户
</span></span><span style="display:flex;"><span>  │
</span></span><span style="display:flex;"><span>  ▼
</span></span><span style="display:flex;"><span>Azure VM
</span></span><span style="display:flex;"><span>  ├── OpenWebUI Docker 容器
</span></span><span style="display:flex;"><span>  │      └── 连接 VM 宿主机 PostgreSQL
</span></span><span style="display:flex;"><span>  │
</span></span><span style="display:flex;"><span>  └── PostgreSQL
</span></span><span style="display:flex;"><span>         └── 数据保存在 VM 本地磁盘
</span></span></code></pre></div><p>这种部署方式比较简单，但随着应用逐渐稳定运行，数据库维护也成为了新的负担。</p>
<p>需要自行处理的工作包括：</p>
<ul>
<li>PostgreSQL 安全更新和补丁</li>
<li>数据库备份及备份轮换</li>
<li>数据恢复和恢复演练</li>
<li>磁盘容量监控</li>
<li>数据库服务监控</li>
<li>PostgreSQL 版本升级</li>
<li>故障处理</li>
<li>网络和访问权限管理</li>
</ul>
<p>对于 OpenWebUI 这样的应用来说，数据库是重要的状态组件，但并不是应用的核心运维目标。因此，将 PostgreSQL 迁移到 Azure 托管服务是一个更合理的选择。</p>
<h2 id="迁移后的架构">迁移后的架构</h2>
<p>迁移后的架构如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户
</span></span><span style="display:flex;"><span>  │
</span></span><span style="display:flex;"><span>  ▼
</span></span><span style="display:flex;"><span>Azure VM
</span></span><span style="display:flex;"><span>  └── OpenWebUI Docker 容器
</span></span><span style="display:flex;"><span>          │
</span></span><span style="display:flex;"><span>          ├── Private Endpoint
</span></span><span style="display:flex;"><span>          │       └── Azure Database for PostgreSQL
</span></span><span style="display:flex;"><span>          │
</span></span><span style="display:flex;"><span>          └── 宿主机持久化目录
</span></span><span style="display:flex;"><span>                  ├── uploads
</span></span><span style="display:flex;"><span>                  ├── vector_db
</span></span><span style="display:flex;"><span>                  └── cache
</span></span></code></pre></div><p>最终实现了：</p>
<ul>
<li>OpenWebUI 使用 Azure 托管 PostgreSQL</li>
<li>数据库通过 Private Endpoint 私网访问</li>
<li>数据库连接启用 TLS</li>
<li>PostgreSQL 不再占用 VM 本地端口和磁盘</li>
<li>OpenWebUI 上传文件和向量数据实现持久化挂载</li>
<li>Azure 自动执行数据库备份</li>
<li>VM 上只保留 PostgreSQL 客户端，用于日常管理 Azure 数据库</li>
</ul>
<p><img alt="架构" loading="lazy" src="/zh/blog/2026/migrate-openwebui-to-azure-pg/2-architecture.png"></p>
<h2 id="为什么选择-azure-database-for-postgresql">为什么选择 Azure Database for PostgreSQL</h2>
<h3 id="1-降低数据库运维成本">1. 降低数据库运维成本</h3>
<p>Azure Database for PostgreSQL Flexible Server 是一项托管数据库服务。</p>
<p>与 VM 自建 PostgreSQL 相比，Azure 可以帮助处理大量底层工作，例如：</p>
<ul>
<li>数据库基础设施维护</li>
<li>自动备份</li>
<li>事务日志备份</li>
<li>时间点恢复</li>
<li>监控指标采集</li>
<li>存储管理</li>
<li>计划性维护</li>
<li>可选高可用</li>
<li>可选只读副本</li>
</ul>
<p>迁移完成后，VM 只需要负责运行 OpenWebUI，数据库生命周期则交给 Azure 管理。</p>
<h3 id="2-自动备份和时间点恢复">2. 自动备份和时间点恢复</h3>
<p>Azure PostgreSQL 提供内置自动备份，并持续备份事务日志，支持时间点恢复（Point-in-Time Restore，PITR）。</p>
<p>当前环境配置了自动备份保留策略。发生误操作时，可以在保留期内创建一台恢复到指定时间点的新服务器。</p>
<p>与自行编写 <code>pg_dump</code> 和 Cron 脚本相比，Azure 托管备份具有以下优势：</p>
<ul>
<li>不需要维护备份脚本</li>
<li>不需要手动轮换备份文件</li>
<li>不会占用 VM 本地磁盘</li>
<li>可以恢复到保留期内的特定时间</li>
<li>备份操作由平台统一管理</li>
</ul>
<p>需要注意的是，Azure 内置备份主要用于平台恢复，不等同于可随意下载的逻辑备份文件。</p>
<p>如果有以下需求，仍建议额外定期执行 <code>pg_dump</code>：</p>
<ul>
<li>长期离线归档</li>
<li>跨平台恢复</li>
<li>数据审计</li>
<li>跨订阅或跨云迁移</li>
<li>保留期超过平台备份策略</li>
</ul>
<h3 id="3-使用-private-endpoint-私网访问">3. 使用 Private Endpoint 私网访问</h3>
<p>本次迁移没有将 PostgreSQL 直接暴露到公网，而是为 Azure PostgreSQL 配置了 Private Endpoint。</p>
<p>OpenWebUI 访问数据库时，数据库域名通过 Private DNS 解析到 VNet 中的私有 IP：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Azure PostgreSQL FQDN
</span></span><span style="display:flex;"><span>        ↓
</span></span><span style="display:flex;"><span>Private DNS
</span></span><span style="display:flex;"><span>        ↓
</span></span><span style="display:flex;"><span>Private Endpoint 私有 IP
</span></span></code></pre></div><p>从 OpenWebUI 容器内部进行了两层验证：</p>
<ol>
<li>DNS 是否解析到私有地址</li>
<li>TCP 5432 是否可以通过私网访问</li>
</ol>
<p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker exec -i open-webui python3 - <span style="color:#e6db74">&lt;&lt;&#39;PY&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import socket
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">host = &#34;&lt;AZURE_POSTGRESQL_PRIVATE_FQDN&gt;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">port = 5432
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">addresses = sorted({
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    item[4][0]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    for item in socket.getaddrinfo(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        host,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        port,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        type=socket.SOCK_STREAM
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">})
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;Resolved addresses:&#34;, addresses)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">with socket.create_connection((host, port), timeout=5) as sock:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    print(&#34;TCP connection: OK&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    print(&#34;Connected peer:&#34;, sock.getpeername())
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">PY</span>
</span></span></code></pre></div><p>数据库安全性并不只是来自 Private Endpoint，而是来自以下措施的组合：</p>
<ul>
<li>Private Endpoint</li>
<li>Private DNS</li>
<li>VNet 隔离</li>
<li>NSG 和路由控制</li>
<li>禁用或限制公网访问</li>
<li>PostgreSQL 身份认证</li>
<li>TLS 链路加密</li>
<li>最小权限数据库账号</li>
</ul>
<h3 id="4-数据库连接启用-tls">4. 数据库连接启用 TLS</h3>
<p>OpenWebUI 的数据库连接串启用了：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>sslmode=require
</span></span></code></pre></div><p>脱敏后的连接串格式如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>postgresql://&lt;APP_USER&gt;:&lt;PASSWORD&gt;@&lt;AZURE_POSTGRESQL_FQDN&gt;:5432/&lt;APP_DATABASE&gt;?sslmode=require
</span></span></code></pre></div><p>迁移前还通过 OpenWebUI 使用的 Python 和 SQLAlchemy 环境实际验证了 TLS：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> create_engine, text
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>engine <span style="color:#f92672">=</span> create_engine(os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#34;DATABASE_URL&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> engine<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> conn:
</span></span><span style="display:flex;"><span>    ssl_enabled <span style="color:#f92672">=</span> conn<span style="color:#f92672">.</span>execute(text(<span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SELECT ssl
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        FROM pg_stat_ssl
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WHERE pid = pg_backend_pid()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>))<span style="color:#f92672">.</span>scalar_one()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;TLS enabled:&#34;</span>, ssl_enabled)
</span></span></code></pre></div><p>如果安全要求更高，可以进一步使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>sslmode=verify-full
</span></span></code></pre></div><p>这种模式不仅加密连接，还会验证证书和服务器主机名，但需要正确配置 CA 证书，并使用与证书匹配的服务器 FQDN。</p>
<h3 id="5-更容易扩展可靠性能力">5. 更容易扩展可靠性能力</h3>
<p>迁移到 Azure PostgreSQL 后，可以根据 OpenWebUI 的使用规模继续配置：</p>
<ul>
<li>计算规格扩缩容</li>
<li>存储扩容</li>
<li>可用区高可用</li>
<li>只读副本</li>
<li>慢查询分析</li>
<li>数据库连接监控</li>
<li>CPU、内存和存储告警</li>
<li>更长的备份保留期</li>
<li>异地恢复策略</li>
</ul>
<p>这使 OpenWebUI 的数据库层从“单 VM 上的本地服务”升级为独立、可管理的托管数据服务。</p>
<h2 id="迁移目标让-openwebui-尽量保持在线">迁移目标：让 OpenWebUI 尽量保持在线</h2>
<p>传统迁移通常采用以下流程：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>停止应用
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>导出数据库
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>恢复目标数据库
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>修改连接配置
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>重新启动应用
</span></span></code></pre></div><p>这种方式虽然简单，但数据库越大，停机时间越长。</p>
<p>本次迁移采用了不同策略：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>在线全量备份
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>在线恢复到 Azure PostgreSQL
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>验证结构、权限和数据
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>同步迁移期间新增的活跃对话
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>快速重建 OpenWebUI 容器
</span></span></code></pre></div><p>在以下阶段，OpenWebUI 都保持在线：</p>
<ul>
<li>全量 <code>pg_dump</code></li>
<li>Azure PostgreSQL 数据恢复</li>
<li>表结构验证</li>
<li>数据量对比</li>
<li>Private Endpoint 验证</li>
<li>TLS 验证</li>
<li>增量数据同步</li>
</ul>
<p>最终只有更换 <code>DATABASE_URL</code> 并重建容器时发生了短暂连接中断。</p>
<p>因此，更准确地说，这是一种：</p>

<blockquote>
  <p><strong>近零停机、用户侧近乎无感知的 OpenWebUI 数据库迁移方案。</strong></p>

</blockquote>
<h2 id="迁移前检查-openwebui">迁移前检查 OpenWebUI</h2>
<h3 id="1-确认-openwebui-使用的数据库类型">1. 确认 OpenWebUI 使用的数据库类型</h3>
<p>首先检查 OpenWebUI 容器环境变量：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect open-webui <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --format <span style="color:#e6db74">&#39;{{range .Config.Env}}{{println .}}{{end}}&#39;</span> |
</span></span><span style="display:flex;"><span>grep -E <span style="color:#e6db74">&#39;DATABASE_URL|DATA_DIR|WEBUI_SECRET_KEY&#39;</span>
</span></span></code></pre></div><p>确认 OpenWebUI 已经使用 PostgreSQL，而不是默认 SQLite：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>DATABASE_URL=postgresql://&lt;LOCAL_USER&gt;:***@&lt;LOCAL_HOST&gt;:5432/&lt;LOCAL_DATABASE&gt;
</span></span></code></pre></div><p>由于源端和目标端都是 PostgreSQL，因此可以直接使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>pg_dump → pg_restore
</span></span></code></pre></div><p>不需要使用 SQLite 到 PostgreSQL 的转换工具。</p>
<h3 id="2-检查源数据库">2. 检查源数据库</h3>
<p>检查 PostgreSQL 版本和数据库大小：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo -u postgres psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d &lt;LOCAL_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  current_database(),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  current_setting(&#39;server_version&#39;),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  pg_size_pretty(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    pg_database_size(current_database())
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  );
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>同时检查源数据库使用了哪些扩展：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo -u postgres psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d &lt;LOCAL_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT extname, extversion
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">FROM pg_extension
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ORDER BY extname;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>本次源数据库没有使用额外的第三方扩展，因此降低了跨环境迁移的兼容性风险。</p>
<h3 id="3-检查-openwebui-的本地文件">3. 检查 OpenWebUI 的本地文件</h3>
<p>数据库并不是 OpenWebUI 的全部数据。</p>
<p>检查容器挂载后发现，原容器没有配置持久化 volume，而 <code>/app/backend/data</code> 中已经存在大量数据：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/app/backend/data
</span></span><span style="display:flex;"><span>├── cache
</span></span><span style="display:flex;"><span>├── uploads
</span></span><span style="display:flex;"><span>└── vector_db
</span></span></code></pre></div><p>这些内容包括：</p>
<ul>
<li>用户上传文件</li>
<li>向量数据库</li>
<li>缓存</li>
<li>其他应用本地状态</li>
</ul>
<p>它们不会进入 PostgreSQL 的 <code>pg_dump</code>，因此必须单独处理。</p>
<p>这也是本次迁移中一个非常重要的发现：</p>

<blockquote>
  <p>迁移 OpenWebUI 的 PostgreSQL，并不等于迁移了 OpenWebUI 的全部数据。</p>

</blockquote>
<h2 id="准备-azure-postgresql">准备 Azure PostgreSQL</h2>
<h3 id="1-创建独立应用账号">1. 创建独立应用账号</h3>
<p>不建议让 OpenWebUI 长期使用 Azure PostgreSQL 管理员账号。</p>
<p>在目标服务器中创建专用角色：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">ROLE</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span> LOGIN;
</span></span></code></pre></div><p>通过 <code>psql</code> 交互方式设置密码，可以避免密码出现在 Shell 历史记录中：</p>
<pre tabindex="0"><code class="language-psql" data-lang="psql">\password &lt;APP_USER&gt;
</code></pre><p>然后创建目标数据库：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">DATABASE</span> <span style="color:#f92672">&lt;</span>APP_DATABASE<span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">OWNER</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span>;
</span></span></code></pre></div><h3 id="2-验证应用账号">2. 验证应用账号</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;host=&lt;AZURE_POSTGRESQL_FQDN&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   port=5432 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   dbname=&lt;APP_DATABASE&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   user=&lt;APP_USER&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   sslmode=require&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -W <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;SELECT current_user, current_database();&#34;</span>
</span></span></code></pre></div><p>同时确认目标数据库为空：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> information_schema.tables
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> table_schema <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> table_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;BASE TABLE&#39;</span>;
</span></span></code></pre></div><h3 id="3-处理-public-schema-权限">3. 处理 <code>public</code> schema 权限</h3>
<p>第一次测试恢复时遇到了：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ERROR: permission denied for schema public
</span></span></code></pre></div><p>原因是应用账号虽然可以连接数据库，但没有在 <code>public</code> schema 中创建对象的权限。</p>
<p>使用管理员账号连接目标数据库后执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">SCHEMA</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">OWNER</span> <span style="color:#66d9ef">TO</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GRANT</span> <span style="color:#66d9ef">USAGE</span>, <span style="color:#66d9ef">CREATE</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">SCHEMA</span> <span style="color:#66d9ef">public</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">TO</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span>;
</span></span></code></pre></div><p>这个问题说明，仅验证账号“能否登录”还不够，还需要确认该账号能否：</p>
<ul>
<li>创建表</li>
<li>创建索引</li>
<li>创建序列</li>
<li>创建约束</li>
<li>执行 OpenWebUI 的 Alembic 数据库迁移</li>
</ul>
<h2 id="在线执行全量数据库迁移">在线执行全量数据库迁移</h2>
<h3 id="1-在线导出-openwebui-数据库">1. 在线导出 OpenWebUI 数据库</h3>
<p>PostgreSQL 的 <code>pg_dump</code> 会基于一致性事务快照导出数据，因此可以在 OpenWebUI 继续运行时执行。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo -u postgres pg_dump <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --dbname<span style="color:#f92672">=</span>&lt;LOCAL_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --format<span style="color:#f92672">=</span>custom <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-owner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-acl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --file<span style="color:#f92672">=</span>/tmp/openwebui-pre-migration.dump
</span></span></code></pre></div><p>参数含义：</p>
<ul>
<li><code>--format=custom</code>：使用 PostgreSQL 自定义归档格式</li>
<li><code>--no-owner</code>：不恢复源端对象 owner</li>
<li><code>--no-acl</code>：不恢复源端权限</li>
<li><code>--file</code>：指定备份文件</li>
</ul>
<p>检查归档是否有效：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pg_restore <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --list <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /tmp/openwebui-pre-migration.dump |
</span></span><span style="display:flex;"><span>head -30
</span></span></code></pre></div><p>自定义格式会保留：</p>
<ul>
<li>表结构</li>
<li>表数据</li>
<li>主键</li>
<li>唯一约束</li>
<li>索引</li>
<li>外键</li>
<li>序列</li>
<li>Alembic 迁移版本</li>
</ul>
<h3 id="2-恢复到-azure-postgresql">2. 恢复到 Azure PostgreSQL</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pg_restore <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --host<span style="color:#f92672">=</span>&lt;AZURE_POSTGRESQL_FQDN&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --port<span style="color:#f92672">=</span><span style="color:#ae81ff">5432</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --username<span style="color:#f92672">=</span>&lt;APP_USER&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --dbname<span style="color:#f92672">=</span>&lt;APP_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-owner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-acl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --exit-on-error <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --verbose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /tmp/openwebui-pre-migration.dump
</span></span></code></pre></div><p>使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>--exit-on-error
</span></span></code></pre></div><p>可以确保遇到恢复错误时立即停止，避免忽略关键问题。</p>
<p>恢复成功后更新统计信息：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;host=&lt;AZURE_POSTGRESQL_FQDN&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   port=5432 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   dbname=&lt;APP_DATABASE&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   user=&lt;APP_USER&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   sslmode=require&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -W <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;ANALYZE;&#34;</span>
</span></span></code></pre></div><h2 id="对比源端与目标端数据">对比源端与目标端数据</h2>
<p>恢复完成后，需要对 OpenWebUI 的核心表进行对比。</p>
<p>示例查询：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">&#34;user&#34;</span>) <span style="color:#66d9ef">AS</span> users,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> chat) <span style="color:#66d9ef">AS</span> chats,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> chat_message) <span style="color:#66d9ef">AS</span> chat_messages,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> file) <span style="color:#66d9ef">AS</span> files,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> knowledge) <span style="color:#66d9ef">AS</span> knowledge,
</span></span><span style="display:flex;"><span>  (
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">FROM</span> information_schema.tables
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">WHERE</span> table_schema <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">AND</span> table_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;BASE TABLE&#39;</span>
</span></span><span style="display:flex;"><span>  ) <span style="color:#66d9ef">AS</span> tables;
</span></span></code></pre></div><p>重点检查：</p>
<ul>
<li>用户数量</li>
<li>对话数量</li>
<li>消息数量</li>
<li>文件记录数量</li>
<li>知识库数量</li>
<li>业务表数量</li>
<li>Alembic 版本</li>
<li>表 owner</li>
<li>序列状态</li>
</ul>
<p>对比时发现，目标端的对话消息比源端少了少量记录。</p>
<p>这并不是恢复失败，而是因为：</p>

<blockquote>
  <p>在全量备份和恢复过程中，OpenWebUI 仍然在线，当前迁移操作的对话还在不断产生新消息。</p>

</blockquote>
<p>因此，下一步需要同步全量快照之后新增的数据。</p>
<h2 id="同步迁移期间新增的活跃对话">同步迁移期间新增的活跃对话</h2>
<p>本次迁移期间，主要只有当前操作对话仍在更新，因此没有再次恢复整个数据库，而是只同步当前活跃对话。</p>
<p>涉及两张表：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>chat
</span></span><span style="display:flex;"><span>chat_message
</span></span></code></pre></div><h3 id="1-找到最近更新的对话">1. 找到最近更新的对话</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.id,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.title,
</span></span><span style="display:flex;"><span>  to_timestamp(<span style="color:#66d9ef">c</span>.updated_at) <span style="color:#66d9ef">AS</span> updated_at,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">count</span>(cm.id) <span style="color:#66d9ef">AS</span> message_count
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> chat <span style="color:#66d9ef">c</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LEFT</span> <span style="color:#66d9ef">JOIN</span> chat_message cm
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">ON</span> cm.chat_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">c</span>.id
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GROUP</span> <span style="color:#66d9ef">BY</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.id,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.title,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.updated_at
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">c</span>.updated_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">5</span>;
</span></span></code></pre></div><p>确定活跃对话后，后续脚本通过环境变量或参数接收对话 ID，而不是将其直接写入公开文档：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>CHAT_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;ACTIVE_CHAT_ID&gt;&#34;</span>
</span></span></code></pre></div><h3 id="2-使用一致性快照导出对话数据">2. 使用一致性快照导出对话数据</h3>
<p>为了保证 <code>chat</code> 和 <code>chat_message</code> 来自同一个时间点，使用只读、可重复读事务：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span> <span style="color:#66d9ef">TRANSACTION</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ISOLATION</span> <span style="color:#66d9ef">LEVEL</span> <span style="color:#66d9ef">REPEATABLE</span> <span style="color:#66d9ef">READ</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">READ</span> <span style="color:#66d9ef">ONLY</span>;
</span></span></code></pre></div><p>然后在同一事务中导出：</p>
<pre tabindex="0"><code class="language-psql" data-lang="psql">\copy (
  SELECT *
  FROM chat
  WHERE id = &#39;&lt;ACTIVE_CHAT_ID&gt;&#39;
) TO &#39;/tmp/chat.csv&#39;
WITH (FORMAT CSV);
</code></pre><pre tabindex="0"><code class="language-psql" data-lang="psql">\copy (
  SELECT *
  FROM chat_message
  WHERE chat_id = &#39;&lt;ACTIVE_CHAT_ID&gt;&#39;
  ORDER BY created_at, id
) TO &#39;/tmp/chat-messages.csv&#39;
WITH (FORMAT CSV);
</code></pre><p>最后提交：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></div><p>需要注意：<code>psql</code> 的 <code>\copy</code> 是客户端元命令，复杂字段列表最好写在同一行，否则可能出现：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>\copy: parse error at end of line
</span></span></code></pre></div><h3 id="3-在-azure-中使用临时表和-upsert">3. 在 Azure 中使用临时表和 UPSERT</h3>
<p>目标端创建临时 staging 表：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> TEMP <span style="color:#66d9ef">TABLE</span> stage_chat
</span></span><span style="display:flex;"><span>(<span style="color:#66d9ef">LIKE</span> <span style="color:#66d9ef">public</span>.chat <span style="color:#66d9ef">INCLUDING</span> <span style="color:#66d9ef">DEFAULTS</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> TEMP <span style="color:#66d9ef">TABLE</span> stage_chat_message
</span></span><span style="display:flex;"><span>(<span style="color:#66d9ef">LIKE</span> <span style="color:#66d9ef">public</span>.chat_message <span style="color:#66d9ef">INCLUDING</span> <span style="color:#66d9ef">DEFAULTS</span>);
</span></span></code></pre></div><p>将 CSV 导入临时表后，对 <code>chat</code> 主记录执行 UPSERT：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> <span style="color:#66d9ef">public</span>.chat (...)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> ...
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> stage_chat
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> CONFLICT (id) <span style="color:#66d9ef">DO</span> <span style="color:#66d9ef">UPDATE</span> <span style="color:#66d9ef">SET</span>
</span></span><span style="display:flex;"><span>  title              <span style="color:#f92672">=</span> EXCLUDED.title,
</span></span><span style="display:flex;"><span>  updated_at         <span style="color:#f92672">=</span> EXCLUDED.updated_at,
</span></span><span style="display:flex;"><span>  chat               <span style="color:#f92672">=</span> EXCLUDED.chat,
</span></span><span style="display:flex;"><span>  meta               <span style="color:#f92672">=</span> EXCLUDED.meta,
</span></span><span style="display:flex;"><span>  current_message_id <span style="color:#f92672">=</span> EXCLUDED.current_message_id;
</span></span></code></pre></div><p>对消息表执行类似操作：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> <span style="color:#66d9ef">public</span>.chat_message (...)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> ...
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> stage_chat_message
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> CONFLICT (id) <span style="color:#66d9ef">DO</span> <span style="color:#66d9ef">UPDATE</span> <span style="color:#66d9ef">SET</span>
</span></span><span style="display:flex;"><span>  content         <span style="color:#f92672">=</span> EXCLUDED.content,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">output</span>          <span style="color:#f92672">=</span> EXCLUDED.<span style="color:#66d9ef">output</span>,
</span></span><span style="display:flex;"><span>  status_history  <span style="color:#f92672">=</span> EXCLUDED.status_history,
</span></span><span style="display:flex;"><span>  error           <span style="color:#f92672">=</span> EXCLUDED.error,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">usage</span>           <span style="color:#f92672">=</span> EXCLUDED.<span style="color:#66d9ef">usage</span>,
</span></span><span style="display:flex;"><span>  done            <span style="color:#f92672">=</span> EXCLUDED.done,
</span></span><span style="display:flex;"><span>  updated_at      <span style="color:#f92672">=</span> EXCLUDED.updated_at,
</span></span><span style="display:flex;"><span>  context_summary <span style="color:#f92672">=</span> EXCLUDED.context_summary;
</span></span></code></pre></div><p>为了让 Azure 中该对话的消息集合与源快照完全一致，还会删除目标端多余记录：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">FROM</span> <span style="color:#66d9ef">public</span>.chat_message target
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> target.chat_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&lt;ACTIVE_CHAT_ID&gt;&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">EXISTS</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">SELECT</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">FROM</span> stage_chat_message staged
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">WHERE</span> staged.id <span style="color:#f92672">=</span> target.id
</span></span><span style="display:flex;"><span>  );
</span></span></code></pre></div><p>整个导入和 UPSERT 过程放在一个事务中执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- 创建临时表
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">-- 导入数据
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">-- UPSERT
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">-- 清理多余记录
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></div><p>如果任何步骤失败，事务会自动回滚，不会留下部分同步数据。</p>
<h2 id="最终快速切换-openwebui">最终快速切换 OpenWebUI</h2>
<p>完成在线全量迁移后，最终操作为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>最后同步一次当前活跃对话
</span></span><span style="display:flex;"><span>              ↓
</span></span><span style="display:flex;"><span>重建 OpenWebUI 容器
</span></span><span style="display:flex;"><span>              ↓
</span></span><span style="display:flex;"><span>新容器连接 Azure PostgreSQL
</span></span></code></pre></div><p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./sync-active-chat-to-azure.sh <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  up -d --force-recreate
</span></span></code></pre></div><p>完整数据库已经提前迁移并验证，因此最终阶段不需要再次等待几百 MB 数据恢复，只需要同步少量增量记录并快速重建容器。</p>
<p>这将切换窗口缩短到了容器启动所需的时间。</p>
<h2 id="安全保存-openwebui-数据库连接串">安全保存 OpenWebUI 数据库连接串</h2>
<p>数据库连接串没有直接写入 Compose 文件，而是放在独立环境文件中：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>open-webui-azure-db.env
</span></span></code></pre></div><p>文件内容形式：</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">DATABASE_URL=postgresql://&lt;APP_USER&gt;:&lt;URL_ENCODED_PASSWORD&gt;@&lt;AZURE_POSTGRESQL_FQDN&gt;:5432/&lt;APP_DATABASE&gt;?sslmode=require
</code></pre><p>文件权限设置为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chmod <span style="color:#ae81ff">600</span> open-webui-azure-db.env
</span></span></code></pre></div><p>如果密码包含特殊字符，需要先进行 URL 编码：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python3 - <span style="color:#e6db74">&lt;&lt;&#39;PY&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import getpass
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import urllib.parse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">password = getpass.getpass(&#34;Database password: &#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(urllib.parse.quote(password, safe=&#34;&#34;))
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">PY</span>
</span></span></code></pre></div><p>新的 Compose 文件示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">open-webui</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/open-webui/open-webui:&lt;VERSION&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">open-webui</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8080:8080&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env_file</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./open-webui.env</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./open-webui-azure-db.env</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/opt/open-webui/data:/app/backend/data</span>
</span></span></code></pre></div><h2 id="持久化-openwebui-本地文件">持久化 OpenWebUI 本地文件</h2>
<p>迁移前，OpenWebUI 的 <code>/app/backend/data</code> 没有挂载到宿主机。</p>
<p>先将数据复制出来：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo install <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -m <span style="color:#ae81ff">700</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>id -un<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -g <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>id -gn<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /opt/open-webui/data
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker cp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  open-webui:/app/backend/data/. <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /opt/open-webui/data/
</span></span></code></pre></div><p>在 Compose 中加入：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">/opt/open-webui/data:/app/backend/data</span>
</span></span></code></pre></div><p>这样，即使未来重建或升级 OpenWebUI 容器，以下内容仍会保留：</p>
<ul>
<li>上传文件</li>
<li>向量数据库</li>
<li>缓存</li>
<li>其他本地应用数据</li>
</ul>
<p>迁移后验证：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect open-webui <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --format <span style="color:#e6db74">&#39;{{range .Mounts}}{{println .Source &#34;-&gt;&#34; .Destination &#34;rw=&#34; .RW}}{{end}}&#39;</span>
</span></span></code></pre></div><p>预期：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/opt/open-webui/data -&gt; /app/backend/data rw=true
</span></span></code></pre></div><h2 id="迁移后的验证">迁移后的验证</h2>
<h3 id="1-openwebui-健康检查">1. OpenWebUI 健康检查</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -sS <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o /dev/null <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -w <span style="color:#e6db74">&#39;HTTP %{http_code}\n&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --max-time <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  http://127.0.0.1:8080/health
</span></span></code></pre></div><p>预期：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>HTTP 200
</span></span></code></pre></div><h3 id="2-验证-openwebui-当前数据库">2. 验证 OpenWebUI 当前数据库</h3>
<p>为了避免输出密码，只解析连接串中的非敏感部分：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker exec -i open-webui python3 - <span style="color:#e6db74">&lt;&lt;&#39;PY&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import os
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">from urllib.parse import urlsplit
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">u = urlsplit(os.environ[&#34;DATABASE_URL&#34;])
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;Database host configured:&#34;, bool(u.hostname))
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;Database port:&#34;, u.port)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;TLS required:&#34;, &#34;sslmode=require&#34; in u.query)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">PY</span>
</span></span></code></pre></div><p>验证重点：</p>
<ul>
<li>Host 已不再指向 VM 本地 PostgreSQL</li>
<li>端口为 5432</li>
<li>已启用 <code>sslmode=require</code></li>
</ul>
<h3 id="3-检查启动日志">3. 检查启动日志</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker logs <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --since 15m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  open-webui 2&gt;&amp;<span style="color:#ae81ff">1</span> |
</span></span><span style="display:flex;"><span>grep -Ei <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;error|fatal|exception|traceback|permission denied|database|postgres|alembic|ssl|timeout&#39;</span>
</span></span></code></pre></div><p>正常情况下只会看到类似：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO [alembic.runtime.migration] Will assume transactional DDL.
</span></span></code></pre></div><p>不应出现：</p>
<ul>
<li>数据库连接失败</li>
<li>密码认证失败</li>
<li>TLS 错误</li>
<li>Schema 权限错误</li>
<li>Alembic 迁移失败</li>
<li>Python Traceback</li>
</ul>
<h2 id="卸载-vm-本地-postgresql">卸载 VM 本地 PostgreSQL</h2>
<p>确认 OpenWebUI 已稳定连接 Azure PostgreSQL 后，可以删除 VM 上的本地 PostgreSQL。</p>
<p>首先确认：</p>
<ul>
<li>OpenWebUI 健康检查正常</li>
<li>数据库连接已切换</li>
<li>Azure PostgreSQL 中存在最新数据</li>
<li>自动备份策略已经启用</li>
<li>本地 PostgreSQL 不再有应用连接</li>
</ul>
<p>停止并删除本地集群：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo pg_dropcluster <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --stop <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  &lt;POSTGRESQL_VERSION&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  main
</span></span></code></pre></div><p>卸载 PostgreSQL 服务端：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get purge -y <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-&lt;POSTGRESQL_VERSION&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-contrib
</span></span></code></pre></div><p>删除遗留数据和配置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo rm -rf <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /etc/postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/log/postgresql
</span></span></code></pre></div><p>可以继续保留 PostgreSQL 客户端：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>postgresql-client
</span></span><span style="display:flex;"><span>postgresql-client-common
</span></span></code></pre></div><p>这样仍然可以从 VM 使用 <code>psql</code> 管理 Azure PostgreSQL。</p>
<p>最终检查本地 5432：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> sudo ss -lntp | grep -q <span style="color:#e6db74">&#39;:5432&#39;</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  sudo ss -lntp | grep <span style="color:#e6db74">&#39;:5432&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;No local PostgreSQL listener&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><h2 id="迁移过程中的经验总结">迁移过程中的经验总结</h2>
<h3 id="1-数据库迁移不等于-openwebui-全部数据迁移">1. 数据库迁移不等于 OpenWebUI 全部数据迁移</h3>
<p>OpenWebUI 除了 PostgreSQL，还可能在 <code>/app/backend/data</code> 中保存：</p>
<ul>
<li>上传文件</li>
<li>向量数据库</li>
<li>缓存</li>
<li>本地状态文件</li>
</ul>
<p>只执行 <code>pg_dump</code> 并不能迁移这些内容。</p>
<h3 id="2-不要只测试端口连通">2. 不要只测试端口连通</h3>
<p><code>nc</code> 成功只能证明 TCP 5432 可达，并不能证明：</p>
<ul>
<li>PostgreSQL 用户可以登录</li>
<li>密码正确</li>
<li>TLS 正常</li>
<li>数据库权限正确</li>
<li>OpenWebUI 使用的驱动兼容</li>
</ul>
<p>应至少完成以下验证：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>DNS → TCP → PostgreSQL 登录 → TLS → SQL 查询 → OpenWebUI 实际连接
</span></span></code></pre></div><h3 id="3-数据库-owner-不代表一定拥有-schema-创建权限">3. 数据库 owner 不代表一定拥有 schema 创建权限</h3>
<p>即使应用用户是数据库 owner，也可能遇到：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>permission denied for schema public
</span></span></code></pre></div><p>迁移前应验证应用账号是否具备：</p>
<ul>
<li><code>USAGE</code></li>
<li><code>CREATE</code></li>
<li>表和序列所有权</li>
<li>Alembic DDL 权限</li>
</ul>
<h3 id="4-在线全量备份后必须处理增量写入">4. 在线全量备份后必须处理增量写入</h3>
<p><code>pg_dump</code> 提供的是开始备份时的一致性快照。</p>
<p>备份期间 OpenWebUI 新产生的对话和消息不会自动进入已经完成的 dump。因此在线迁移必须增加以下机制之一：</p>
<ul>
<li>最终停写后重新完整备份</li>
<li>逻辑复制</li>
<li>CDC</li>
<li>应用双写</li>
<li>按业务对象执行增量同步</li>
</ul>
<p>本次采用的是“全量快照 + 活跃对话 UPSERT”。</p>
<h3 id="5-不要在迁移时顺便升级-openwebui">5. 不要在迁移时顺便升级 OpenWebUI</h3>
<p>本次迁移始终保持 OpenWebUI 镜像版本不变。</p>
<p>如果同时升级应用和迁移数据库，一旦发生问题，很难快速判断原因来自：</p>
<ul>
<li>PostgreSQL 版本</li>
<li>数据迁移</li>
<li>Azure 网络</li>
<li>数据库权限</li>
<li>OpenWebUI 版本</li>
<li>Alembic schema 变化</li>
</ul>
<p>更稳妥的做法是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>先迁移数据库并稳定运行
</span></span><span style="display:flex;"><span>          ↓
</span></span><span style="display:flex;"><span>再单独升级 OpenWebUI
</span></span></code></pre></div><h2 id="这算不算真正的零停机">这算不算真正的零停机？</h2>
<p>从用户体验看，这次迁移接近零停机：</p>
<ul>
<li>在线完成全量备份</li>
<li>在线恢复目标数据库</li>
<li>在线完成数据验证</li>
<li>在线同步活跃对话</li>
<li>仅在最后重建了一次 OpenWebUI 容器</li>
</ul>
<p>但如果采用严格工程定义，真正的零停机还需要满足：</p>
<ul>
<li>任意时刻都能接受新连接</li>
<li>最后时刻写入绝不丢失</li>
<li>没有单实例容器重启窗口</li>
<li>客户端连接完全不中断</li>
<li>具备自动流量切换能力</li>
</ul>
<p>更大规模的 OpenWebUI 部署可以考虑：</p>
<ul>
<li>PostgreSQL 逻辑复制</li>
<li>Azure Database Migration Service</li>
<li>CDC 增量迁移</li>
<li>应用双写</li>
<li>蓝绿部署</li>
<li>多个 OpenWebUI 实例</li>
<li>反向代理健康检查</li>
<li>连接排空和流量切换</li>
</ul>
<p>对于单 VM、单 OpenWebUI 容器和中小规模数据库，本次采用的方案在复杂度、风险和停机时间之间取得了较好的平衡。</p>
<h2 id="迁移后的日常运维">迁移后的日常运维</h2>
<p>启动 OpenWebUI：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  up -d
</span></span></code></pre></div><p>查看状态：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  ps
</span></span></code></pre></div><p>查看日志：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  logs -f --tail<span style="color:#f92672">=</span><span style="color:#ae81ff">200</span>
</span></span></code></pre></div><p>健康检查：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -sS <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o /dev/null <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -w <span style="color:#e6db74">&#39;HTTP %{http_code}\n&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  http://127.0.0.1:8080/health
</span></span></code></pre></div><p>日常还应重点监控 Azure PostgreSQL 的：</p>
<ul>
<li>CPU 使用率</li>
<li>内存使用情况</li>
<li>存储空间</li>
<li>活跃连接数</li>
<li>失败连接数</li>
<li>查询延迟</li>
<li>自动备份状态</li>
<li>维护事件</li>
<li>Private Endpoint 状态</li>
</ul>
<h2 id="总结">总结</h2>
<p>这次迁移将 OpenWebUI 的数据库从 Azure VM 自建 PostgreSQL 平滑迁移到了 Azure Database for PostgreSQL Flexible Server。</p>
<p>最终实现了：</p>
<ul>
<li>OpenWebUI 数据库由 Azure 托管</li>
<li>通过 Private Endpoint 私网访问</li>
<li>数据库连接启用 TLS</li>
<li>自动备份和时间点恢复</li>
<li>OpenWebUI 本地文件持久化</li>
<li>VM 本地 PostgreSQL 完全下线</li>
<li>本地 5432 端口关闭</li>
<li>全量迁移期间应用持续在线</li>
<li>最终切换对用户影响极低</li>
</ul>
<p>本次最关键的经验是：</p>

<blockquote>
  <p>对 OpenWebUI 进行低停机数据库迁移，不应只关注 <code>pg_dump</code> 和 <code>pg_restore</code>，还必须同时考虑增量写入、Private Endpoint、TLS、数据库权限、Alembic 迁移以及 <code>/app/backend/data</code> 中的本地文件。</p>

</blockquote>
<p>通过“<strong>在线全量迁移、目标端预验证、活跃数据增量同步、快速容器切换</strong>”，可以在不引入复杂 CDC 平台的情况下，将中小规模 OpenWebUI 平滑迁移到 Azure 托管 PostgreSQL。</p>
]]></content:encoded></item><item><title>在 vLLM Semantic Router 中使用 Microsoft Foundry 远程 Embedding 推理</title><link>https://wilsonwu.me/zh/blog/2026/azure-foundry-embeddings-with-vllm-sr/</link><pubDate>Mon, 03 Aug 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/azure-foundry-embeddings-with-vllm-sr/</guid><description>&lt;h2 id="1-引言"&gt;1. 引言&lt;/h2&gt;
&lt;p&gt;在 vLLM Semantic Router 的设计中，Embedding 推理是驱动语义路由的核心信号之一。&lt;/p&gt;
&lt;p&gt;官方教程已经演示了如何使用&lt;strong&gt;远程 Embedding 推理提供商（Remote Embedding Providers）&lt;/strong&gt;替代本地 Embedding 推理服务：&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="1-引言">1. 引言</h2>
<p>在 vLLM Semantic Router 的设计中，Embedding 推理是驱动语义路由的核心信号之一。</p>
<p>官方教程已经演示了如何使用<strong>远程 Embedding 推理提供商（Remote Embedding Providers）</strong>替代本地 Embedding 推理服务：</p>
<p>👉 <a href="https://vllm-sr.ai/docs/tutorials/global/remote-embeddings/">https://vllm-sr.ai/docs/tutorials/global/remote-embeddings/</a></p>
<p>然而，在实际生产环境中，一个更为关键的问题随之而来：</p>

<blockquote>
  <p>如何使用企业级、可扩展的 Embedding 推理服务来支持语义路由？</p>

</blockquote>
<p>本文基于官方教程中的场景，并结合 <strong>Microsoft Foundry Embedding 模型</strong>，演示如何构建一个具备以下特性的智能路由系统：</p>
<ul>
<li>无需 GPU</li>
<li>无需在本地部署 Embedding 模型</li>
<li>生产就绪</li>
</ul>
<h2 id="2-回顾远程-embedding-推理提供商">2. 回顾：远程 Embedding 推理提供商</h2>
<p>在 vLLM Semantic Router 中，路由器运行在 CPU 上，不依赖 GPU 推理能力。</p>
<p>通过远程 Embedding 推理提供商：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>路由器 → 远程 Embedding 推理 API → 向量
</span></span></code></pre></div><p>我们可以：</p>
<ul>
<li>将嵌入计算完全卸载到云服务</li>
<li>统一多模型的语义能力</li>
<li>解耦路由与推理</li>
</ul>
<h2 id="3-教程场景沿用官方测试场景">3. 教程场景（沿用官方测试场景）</h2>
<p>在官方教程中，核心流程如下：</p>
<p><img alt="远程嵌入流程" loading="lazy" src="/zh/blog/2026/azure-foundry-embeddings-with-vllm-sr/1-remote-embeddings-flow.png"></p>
<h3 id="第-1-步定义语义路由">第 1 步：定义语义路由</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">decisions</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">math</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">semantic_similarity</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">text</span>: <span style="color:#e6db74">&#34;math question&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">threshold</span>: <span style="color:#ae81ff">0.8</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">general</span>
</span></span></code></pre></div><h3 id="第-2-步发送请求">第 2 步：发送请求</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl http://localhost:8888/v1/chat/completions <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;model&#34;: &#34;MoM&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;What is 1+1?&#34;}]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }&#39;</span>
</span></span></code></pre></div><p>路由器将执行以下操作：</p>
<ol>
<li>生成嵌入向量</li>
<li>执行语义匹配</li>
<li>选择目标模型</li>
</ol>
<h2 id="4-第-0-步在-microsoft-foundry-中创建-embedding-模型">4. 第 0 步：在 Microsoft Foundry 中创建 Embedding 模型</h2>
<p>在与 vLLM Semantic Router 集成之前，我们首先需要在 Microsoft Foundry 中部署一个 Embedding 模型。</p>
<h3 id="41-创建-foundry-项目">4.1 创建 Foundry 项目</h3>
<ol>
<li>前往 <strong>Microsoft Foundry 门户</strong></li>
<li>创建新项目（或选择现有项目）</li>
<li>确保该项目已关联 Azure 订阅</li>
</ol>
<h3 id="42-部署嵌入模型">4.2 部署嵌入模型</h3>
<p>在模型目录中：</p>
<ol>
<li>搜索 <strong>Embedding 模型</strong></li>
<li>选择受支持的模型（例如 <code>text-embedding-3-small</code>）</li>
<li>单击 <strong>Deploy（部署）</strong></li>
</ol>
<p>部署完成后，你将获得：</p>
<ul>
<li>Endpoint URL</li>
<li>API Key</li>
<li>Model Name</li>
</ul>
<p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Endpoint: https://&lt;your-foundry-resource&gt;.inference.ai.azure.com
</span></span><span style="display:flex;"><span>Model: text-embedding-3-small
</span></span><span style="display:flex;"><span>API Key: ******
</span></span></code></pre></div><h3 id="43-验证-embedding-推理-api">4.3 验证 Embedding 推理 API</h3>
<p>你可以直接测试该终结点：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl https://&lt;your-foundry-endpoint&gt;/v1/embeddings <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;api-key: &lt;your-api-key&gt;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;input&#34;: &#34;Hello world&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }&#39;</span>
</span></span></code></pre></div><p>如果调用成功，你将收到包含向量的响应。</p>
<h2 id="5-升级使用-microsoft-foundry-替代本地-embedding-模型">5. 升级：使用 Microsoft Foundry 替代本地 Embedding 模型</h2>
<p>现在，我们将教程中的 Embedding 推理提供商替换为：</p>

<blockquote>
  <p>✅ Microsoft Foundry Embedding 模型</p>

</blockquote>
<p><img alt="Microsoft Foundry Embedding 模型" loading="lazy" src="/zh/blog/2026/azure-foundry-embeddings-with-vllm-sr/2-microsoft-foundry-embedding-model.png"></p>
<h2 id="6-为什么选择-microsoft-foundry-embedding-模型">6. 为什么选择 Microsoft Foundry Embedding 模型</h2>
<p>Microsoft Foundry 提供：</p>
<ul>
<li>高质量 Embedding 模型（更强的语义理解能力）</li>
<li>托管 API（无需自行部署）</li>
<li>全球可用性与企业级 SLA</li>
<li>与 Azure AI 生态系统无缝集成</li>
</ul>
<p>更重要的是：</p>

<blockquote>
  <p>它可以直接充当 Semantic Router 的“语义大脑”</p>

</blockquote>
<h2 id="7-集成将-microsoft-foundry-用作远程-embedding-推理提供商">7. 集成：将 Microsoft Foundry 用作远程 Embedding 推理提供商</h2>
<h3 id="71-配置-embedding-推理提供商">7.1 配置 Embedding 推理提供商</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">providers</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">embedding</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">remote</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">provider</span>: <span style="color:#ae81ff">openai-compatible</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">base_url</span>: <span style="color:#ae81ff">https://&lt;your-foundry-endpoint&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">api_key</span>: <span style="color:#ae81ff">&lt;your-api-key&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">model</span>: <span style="color:#ae81ff">text-embedding-3-small</span>
</span></span></code></pre></div><p>说明：</p>
<ul>
<li>Foundry 提供兼容 OpenAI 的 API</li>
<li>可以直接复用 Semantic Router 的远程 Embedding 推理提供商</li>
<li>无需额外适配</li>
</ul>
<h3 id="72-路由器工作流">7.2 路由器工作流</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户请求
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>Semantic Router
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>Microsoft Foundry Embedding 推理 API
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>向量相似度计算
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>路由决策
</span></span><span style="display:flex;"><span>   ↓
</span></span><span style="display:flex;"><span>目标模型（vLLM / API）
</span></span></code></pre></div><h2 id="8-相比官方教程的主要改进">8. 相比官方教程的主要改进</h2>
<table>
	<thead>
			<tr>
					<th>能力</th>
					<th>原始方案</th>
					<th>Microsoft Foundry 方案</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>嵌入来源</td>
					<td>本地 / 模拟</td>
					<td>云端高质量模型</td>
			</tr>
			<tr>
					<td>部署成本</td>
					<td>需要模型或容器</td>
					<td>零运维</td>
			</tr>
			<tr>
					<td>可扩展性</td>
					<td>单一环境</td>
					<td>多区域</td>
			</tr>
			<tr>
					<td>语义质量</td>
					<td>中等</td>
					<td>企业级</td>
			</tr>
	</tbody>
</table>
<h2 id="9-实际价值">9. 实际价值</h2>
<h3 id="-1-真正的无-gpu-架构">🚀 1. 真正的“无 GPU 架构”</h3>
<p>路由器本身运行在 CPU 上。</p>
<p>Embedding 模型与推理能力完全外部化：</p>
<ul>
<li>Foundry → Embedding 模型</li>
<li>vLLM / API → 推理</li>
</ul>
<h3 id="-2-更准确的语义路由">🧠 2. 更准确的语义路由</h3>
<p>在教程场景中：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>&#34;What is 1+1?&#34; → math 路由
</span></span><span style="display:flex;"><span>&#34;Tell me a joke&#34; → general 路由
</span></span></code></pre></div><p>使用 Foundry 嵌入后，可以获得：</p>
<ul>
<li>更稳定的语义理解</li>
<li>更好的多语言支持</li>
<li>对边界场景更准确的判断</li>
</ul>
<h3 id="-3-统一的多模型入口">🌐 3. 统一的多模型入口</h3>
<p>结合 Semantic Router：</p>
<ul>
<li>Microsoft Foundry（Embedding 模型）</li>
<li>vLLM（自托管推理）</li>
<li>OpenAI / 其他 API</li>
</ul>
<p>从而实现真正的：</p>

<blockquote>
  <p><strong>多提供商智能路由</strong></p>

</blockquote>
<h2 id="10-架构洞察">10. 架构洞察</h2>
<p>此次升级的本质是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>升级前：
</span></span><span style="display:flex;"><span>路由器 = 基础设施 + 智能
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>升级后：
</span></span><span style="display:flex;"><span>路由器 = 智能
</span></span><span style="display:flex;"><span>Foundry = AI 能力
</span></span></code></pre></div><p>由此形成清晰的分层架构：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[ AI 能力层 ] → Microsoft Foundry
</span></span><span style="display:flex;"><span>[ 路由层    ] → Semantic Router
</span></span><span style="display:flex;"><span>[ 推理层    ] → vLLM / APIs
</span></span></code></pre></div><h2 id="11-总结">11. 总结</h2>
<p>通过将 Microsoft Foundry 与官方远程 Embedding 模型教程相结合，我们实现了一套生产就绪的架构：</p>
<ul>
<li>无需本地 Embedding 模型</li>
<li>无需 GPU</li>
<li>高质量的语义能力</li>
<li>企业级可靠性</li>
</ul>
<p>这不仅是一次功能增强，更是 Semantic Router 的一次关键演进：</p>

<blockquote>
  <p>从“本地工具”演进为“云原生 AI 路由层”</p>

</blockquote>
]]></content:encoded></item><item><title>让模型自己选模型：Embedding 驱动的 LLM 智能路由机制</title><link>https://wilsonwu.me/zh/blog/2026/llm-smart-router/</link><pubDate>Tue, 26 May 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/llm-smart-router/</guid><description>&lt;p&gt;在多模型并存的 AI 架构中（如 GPT-4 / GPT-4o / 轻量模型 / 垂直模型等），一个核心问题是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如何在不显式指定模型 ID 的情况下，让系统自动选择最合适的模型？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description><content:encoded><![CDATA[<p>在多模型并存的 AI 架构中（如 GPT-4 / GPT-4o / 轻量模型 / 垂直模型等），一个核心问题是：</p>

<blockquote>
  <p><strong>如何在不显式指定模型 ID 的情况下，让系统自动选择最合适的模型？</strong></p>

</blockquote>
<p>本文介绍一种工程可落地的方案：</p>

<blockquote>
  <p><strong>通过 Embedding 模型计算用户意图 → 在网关层进行语义匹配 → 动态路由到最合适的上游模型服务</strong></p>

</blockquote>
<p>我们将基于：</p>
<ul>
<li>Embedding 模型：Azure OpenAI <code>text-embedding-3-small</code></li>
<li>网关：Envoy</li>
<li>核心机制：向量相似度打分 + 策略路由</li>
</ul>
<h2 id="问题背景与动机">问题背景与动机</h2>
<p>在真实生产环境中，大模型调用通常面临几个挑战：</p>
<h3 id="1-成本与性能权衡">1. 成本与性能权衡</h3>
<table>
	<thead>
			<tr>
					<th>模型类型</th>
					<th>优点</th>
					<th>缺点</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>大模型（如 GPT-4）</td>
					<td>能力强</td>
					<td>成本高 / 延迟高</td>
			</tr>
			<tr>
					<td>小模型（如 GPT-4o-mini）</td>
					<td>快 / 便宜</td>
					<td>能力有限</td>
			</tr>
			<tr>
					<td>专用模型</td>
					<td>精准</td>
					<td>泛化弱</td>
			</tr>
	</tbody>
</table>
<p>不同请求应该走不同模型，而不是“一刀切”。</p>
<h3 id="2-传统做法的局限">2. 传统做法的局限</h3>
<p>常见策略：</p>
<ul>
<li>手动指定 model_id ❌（不智能）</li>
<li>基于规则（if/else）❌（不可扩展）</li>
<li>基于关键词匹配 ❌（语义不鲁棒）</li>
</ul>
<h3 id="3-理想目标">3. 理想目标</h3>
<p>我们希望：</p>

<blockquote>
  <p>用户只输入 prompt，系统自动理解“意图”，并选择最优模型</p>

</blockquote>
<p>这正是 Embedding 可以发挥作用的地方。</p>
<h2 id="核心原理embedding-驱动的语义路由">核心原理：Embedding 驱动的语义路由</h2>
<h3 id="1-embedding-本质">1. Embedding 本质</h3>
<p>Embedding 模型（如 <code>text-embedding-3-small</code>）可以将文本映射为高维向量：</p>
<pre tabindex="0"><code>&#34;写一段 Python 排序代码&#34;
→ [0.012, -0.98, ..., 0.33]  (1536维向量)
</code></pre><p>语义相似的文本，其向量距离更近。</p>
<h3 id="2-路由思路">2. 路由思路</h3>
<p>核心逻辑可以抽象为：</p>
<pre tabindex="0"><code>用户请求 → Embedding → 与“意图模板库”计算相似度 → 选择最佳匹配 → 路由模型
</code></pre><h3 id="3-意图模板intent-prototypes">3. 意图模板（Intent Prototypes）</h3>
<p>我们预先定义一组“意图向量”：</p>
<table>
	<thead>
			<tr>
					<th>意图类型</th>
					<th>示例文本</th>
					<th>目标模型</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>代码生成</td>
					<td>&ldquo;write code&rdquo;, &ldquo;generate function&rdquo;</td>
					<td>GPT-4</td>
			</tr>
			<tr>
					<td>简单问答</td>
					<td>&ldquo;what is&rdquo;, &ldquo;define&rdquo;</td>
					<td>GPT-4o-mini</td>
			</tr>
			<tr>
					<td>翻译</td>
					<td>&ldquo;translate this&rdquo;</td>
					<td>轻量模型</td>
			</tr>
			<tr>
					<td>长文生成</td>
					<td>&ldquo;write blog&rdquo;</td>
					<td>GPT-4</td>
			</tr>
	</tbody>
</table>
<p>这些文本会提前计算 embedding 并缓存。</p>
<h3 id="4-相似度计算">4. 相似度计算</h3>
<p>常用方法：</p>
<ul>
<li>Cosine Similarity（推荐）</li>
</ul>
<pre tabindex="0"><code>similarity = cosine(query_embedding, intent_embedding)
</code></pre><p>选择最高分：</p>
<pre tabindex="0"><code>argmax(similarity_i)
</code></pre><h2 id="系统架构设计">系统架构设计</h2>
<p>整体架构如下：</p>
<pre tabindex="0"><code>                ┌──────────────┐
                │   Client     │
                └──────┬───────┘
                       │
               ┌───────▼────────┐
               │    Envoy 网关   │
               │   (智能路由层)   │
               └───────┬────────┘
                       │
        ┌──────────────┼──────────────┐
        │              │              │
 ┌──────▼─────┐ ┌──────▼─────┐ ┌──────▼─────┐
 │ GPT-4      │ │ GPT-4o-mini│ │ 专用模型    │
 └────────────┘ └────────────┘ └────────────┘
</code></pre><h2 id="实现逻辑拆解">实现逻辑拆解</h2>
<h3 id="1-请求流程">1. 请求流程</h3>
<h4 id="step-1获取用户请求">Step 1：获取用户请求</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;input&#34;</span>: <span style="color:#e6db74">&#34;帮我写一段 Go 的 HTTP server 示例&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="step-2调用-embedding-api">Step 2：调用 Embedding API</h4>
<p>使用 Azure OpenAI：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-http" data-lang="http"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">POST https://{endpoint}/openai/deployments/text-embedding-3-small/embeddings?api-version=2024-02-15-preview
</span></span></span></code></pre></div><p>请求体：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;input&#34;</span>: <span style="color:#e6db74">&#34;帮我写一段 Go 的 HTTP server 示例&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="step-3计算相似度">Step 3：计算相似度</h4>
<p>伪代码：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">route</span>(query_embedding):
</span></span><span style="display:flex;"><span>    best_score <span style="color:#f92672">=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    best_model <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> intent <span style="color:#f92672">in</span> intents:
</span></span><span style="display:flex;"><span>        score <span style="color:#f92672">=</span> cosine_similarity(query_embedding, intent<span style="color:#f92672">.</span>embedding)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> score <span style="color:#f92672">&gt;</span> best_score:
</span></span><span style="display:flex;"><span>            best_score <span style="color:#f92672">=</span> score
</span></span><span style="display:flex;"><span>            best_model <span style="color:#f92672">=</span> intent<span style="color:#f92672">.</span>model
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> best_model
</span></span></code></pre></div><h4 id="step-4envoy-动态路由">Step 4：Envoy 动态路由</h4>
<p>将结果写入 Header：</p>
<pre tabindex="0"><code>x-llm-route: gpt-4
</code></pre><p>Envoy 根据 Header 路由：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">routes</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">prefix</span>: <span style="color:#e6db74">&#34;/v1/chat&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">x-llm-route</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exact_match</span>: <span style="color:#ae81ff">gpt-4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">route</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cluster</span>: <span style="color:#ae81ff">gpt4_service</span>
</span></span></code></pre></div><h2 id="envoy-网关实现方式">Envoy 网关实现方式</h2>
<p>Envoy 本身不直接支持向量计算，因此需要扩展：</p>
<h3 id="方案-1external-processing推荐">方案 1：External Processing（推荐）</h3>
<p>Envoy 调用外部服务（ext_proc）：</p>
<pre tabindex="0"><code>Envoy → ext_proc 服务 → 返回路由决策 → Envoy 转发
</code></pre><p>ext_proc 负责：</p>
<ul>
<li>调用 embedding</li>
<li>计算相似度</li>
<li>返回 header</li>
</ul>
<h3 id="方案-2wasm-插件">方案 2：WASM 插件</h3>
<p>在 Envoy 内嵌逻辑：</p>
<ul>
<li>优点：低延迟</li>
<li>缺点：开发复杂（Rust/CPP）</li>
</ul>
<h3 id="方案-3sidecar-router简单">方案 3：Sidecar Router（简单）</h3>
<pre tabindex="0"><code>Client → Router Service → Envoy → Model
</code></pre><p>Router 服务负责所有智能逻辑。</p>
<h2 id="优化策略">优化策略</h2>
<h3 id="1-向量缓存">1. 向量缓存</h3>
<p>避免重复调用 embedding：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>cache[input_hash] <span style="color:#f92672">=</span> embedding
</span></span></code></pre></div><h3 id="2-意图分层">2. 意图分层</h3>
<p>避免单层分类过粗：</p>
<pre tabindex="0"><code>一级：代码 / 问答 / 写作
二级：语言 / 长度 / 难度
</code></pre><h3 id="3-阈值控制">3. 阈值控制</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> best_score <span style="color:#f92672">&lt;</span> threshold:
</span></span><span style="display:flex;"><span>    fallback_model <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;gpt-4&#34;</span>
</span></span></code></pre></div><p>防止误判。</p>
<h3 id="4-多模型融合ensemble">4. 多模型融合（Ensemble）</h3>
<p>可扩展为：</p>
<ul>
<li>Top-K 模型候选</li>
<li>A/B 测试</li>
<li>加权路由</li>
</ul>
<h3 id="5-成本感知路由">5. 成本感知路由</h3>
<p>结合：</p>
<ul>
<li>token 预算</li>
<li>用户等级</li>
<li>SLA</li>
</ul>
<p>做策略增强：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> user <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;free&#34;</span>:
</span></span><span style="display:flex;"><span>    prefer_small_model()
</span></span></code></pre></div><h2 id="优势分析">优势分析</h2>
<h3 id="相比规则路由">相比规则路由</h3>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>Embedding 路由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>语义理解</td>
					<td>强</td>
			</tr>
			<tr>
					<td>可扩展性</td>
					<td>高</td>
			</tr>
			<tr>
					<td>维护成本</td>
					<td>低</td>
			</tr>
	</tbody>
</table>
<h3 id="相比手动指定模型">相比手动指定模型</h3>
<ul>
<li>用户体验更好（无需关心模型）</li>
<li>自动最优成本</li>
<li>支持动态策略</li>
</ul>
<h2 id="落地建议">落地建议</h2>
<h3 id="1-初始阶段">1. 初始阶段</h3>
<ul>
<li>5~10 个意图模板即可</li>
<li>手动标注 + 调优</li>
</ul>
<h3 id="2-数据驱动优化">2. 数据驱动优化</h3>
<p>记录：</p>
<ul>
<li>用户请求</li>
<li>路由结果</li>
<li>用户反馈</li>
</ul>
<p>持续优化 intent embedding。</p>
<h3 id="3-与-llm-router-结合">3. 与 LLM Router 结合</h3>
<p>未来可以升级为：</p>

<blockquote>
  <p>用小模型 + embedding 混合决策（Hybrid Router）</p>

</blockquote>
<h2 id="总结">总结</h2>
<p>本文方案的本质是：</p>

<blockquote>
  <p><strong>用 Embedding 替代规则，用向量相似度驱动模型选择</strong></p>

</blockquote>
<p>关键收益：</p>
<ul>
<li>降低成本（智能选择轻量模型）</li>
<li>提升效果（复杂任务走强模型）</li>
<li>架构解耦（路由逻辑与模型解耦）</li>
</ul>
<p>在多模型时代，这种“语义驱动的网关”会成为标准架构组件。</p>
]]></content:encoded></item><item><title>AI 编程进入 Skill 时代：从提示词到“能力封装”的跃迁</title><link>https://wilsonwu.me/zh/blog/2026/ai-skills/</link><pubDate>Wed, 22 Apr 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/ai-skills/</guid><description>&lt;p&gt;&lt;img alt="AI 编程 Skill 时代" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/ai-skills/1-ai-skills.png"&gt;&lt;/p&gt;
&lt;p&gt;在过去一年里，AI 编程的主流交互方式经历了一个明显演进：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Prompt → Prompt Engineering → Context Engineering → &lt;strong&gt;Skill（能力封装）&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果说 Prompt 是一次性调用模型能力，那么 &lt;strong&gt;Skill 本质上是在构建“可复用的智能能力模块”&lt;/strong&gt;，它让 AI 从“会回答问题”，进化为“能执行任务”。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="AI 编程 Skill 时代" loading="lazy" src="/zh/blog/2026/ai-skills/1-ai-skills.png"></p>
<p>在过去一年里，AI 编程的主流交互方式经历了一个明显演进：</p>

<blockquote>
  <p>Prompt → Prompt Engineering → Context Engineering → <strong>Skill（能力封装）</strong></p>

</blockquote>
<p>如果说 Prompt 是一次性调用模型能力，那么 <strong>Skill 本质上是在构建“可复用的智能能力模块”</strong>，它让 AI 从“会回答问题”，进化为“能执行任务”。</p>
<p>这篇文章将系统讲清：</p>
<ul>
<li>什么是 AI 编程中的 Skill</li>
<li>Skill 与 Prompt 的本质区别</li>
<li>如何在 VS Code + GitHub Copilot Chat 中使用 Skill 思维</li>
<li>实战案例（工程级）</li>
</ul>
<h2 id="什么是-skill">什么是 Skill？</h2>
<p>在 AI 编程语境下，<strong>Skill = 结构化提示 + 上下文 + 执行约束 + 输出格式</strong></p>
<p>可以用一个更工程化的定义：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Skill = f(Instruction, Context, Constraints, Output Schema)
</span></span></code></pre></div><p>它不是一句 prompt，而是一个“可复用的能力接口”。</p>
<h3 id="对比理解">对比理解</h3>
<table>
	<thead>
			<tr>
					<th>方式</th>
					<th>特点</th>
					<th>问题</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Prompt</td>
					<td>临时输入</td>
					<td>不可复用、不稳定</td>
			</tr>
			<tr>
					<td>Prompt Engineering</td>
					<td>优化表达</td>
					<td>仍然是一次性</td>
			</tr>
			<tr>
					<td>Skill</td>
					<td>能力封装</td>
					<td>可复用、可组合、可治理</td>
			</tr>
	</tbody>
</table>
<h2 id="为什么-skill-很重要">为什么 Skill 很重要？</h2>
<p>在实际开发中，AI 使用很快会遇到几个瓶颈：</p>
<h3 id="1-prompt-不稳定">1. Prompt 不稳定</h3>
<p>同一个问题，换个说法结果不同</p>
<h3 id="2-团队无法复用经验">2. 团队无法复用经验</h3>
<p>每个人都有自己的“私有 prompt”</p>
<h3 id="3-ai-输出不可控">3. AI 输出不可控</h3>
<p>格式、风格、边界难以统一</p>
<h2 id="skill-的价值">Skill 的价值</h2>
<ul>
<li><strong>标准化能力</strong></li>
<li><strong>沉淀团队最佳实践</strong></li>
<li><strong>降低认知负担</strong></li>
<li><strong>提升输出一致性</strong></li>
</ul>
<p>本质上：Skill 是 AI 时代的“函数”或“API”</p>
<h2 id="vs-code-中的-skill-实现方式">VS Code 中的 Skill 实现方式</h2>
<p>虽然 VS Code 本身没有官方“Skill”概念，但可以通过以下机制实现：</p>
<h3 id="1-githubcopilot-instructionsmd">1. <code>.github/copilot-instructions.md</code></h3>
<p>这是最接近“全局 Skill”的机制。</p>
<p>作用：</p>
<ul>
<li>为 Copilot 提供<strong>长期上下文</strong></li>
<li>约束 AI 的行为方式</li>
</ul>
<p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Backend Coding Guidelines
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Use clean architecture
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Follow RESTful API design
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Add unit tests for all services
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Use dependency injection
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># Code Style
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Prefer TypeScript over JavaScript
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Use async/await instead of callbacks
</span></span></code></pre></div><p>这相当于一个“全局 Skill 注入器”</p>
<h3 id="2-项目级-skillreadme--projectmd">2. 项目级 Skill（README / project.md）</h3>
<p>你可以在项目中定义：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># AI Skill: API Generator
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>When generating APIs:
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Use OpenAPI 3.0 spec
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Include request/response schema
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Add validation rules
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Provide curl examples
</span></span></code></pre></div><h3 id="3-会话级-skillcopilot-chat">3. 会话级 Skill（Copilot Chat）</h3>
<p>在 Chat 中你可以定义一次性 Skill：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>You are a senior backend architect.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Task:
</span></span><span style="display:flex;"><span>- Refactor code to hexagonal architecture
</span></span><span style="display:flex;"><span>- Keep business logic pure
</span></span><span style="display:flex;"><span>- Extract adapters
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Output:
</span></span><span style="display:flex;"><span>- Folder structure
</span></span><span style="display:flex;"><span>- Example code
</span></span></code></pre></div><h2 id="github-copilot-chat-中的-skill-用法">GitHub Copilot Chat 中的 Skill 用法</h2>
<p>Copilot Chat 是 Skill 最直接的使用场景。</p>
<p>下面给你几个<strong>工程级实战例子</strong>。</p>
<h3 id="示例-1代码重构-skill">示例 1：代码重构 Skill</h3>
<h4 id="输入skill">输入（Skill）</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Act as a senior software architect.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Refactor this code:
</span></span><span style="display:flex;"><span>- Apply clean architecture
</span></span><span style="display:flex;"><span>- Separate domain, application, infrastructure
</span></span><span style="display:flex;"><span>- Add dependency inversion
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Output:
</span></span><span style="display:flex;"><span>1. Folder structure
</span></span><span style="display:flex;"><span>2. Refactored code
</span></span><span style="display:flex;"><span>3. Explanation
</span></span></code></pre></div><h4 id="效果">效果</h4>
<p>AI 不再只是“改代码”，而是：</p>
<ul>
<li>输出结构设计</li>
<li>给出分层方案</li>
<li>解释设计决策</li>
</ul>
<p>从“补全工具”变成“架构助手”</p>
<h3 id="示例-2测试生成-skill">示例 2：测试生成 Skill</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>You are a QA engineer.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Generate unit tests:
</span></span><span style="display:flex;"><span>- Use Jest
</span></span><span style="display:flex;"><span>- Cover edge cases
</span></span><span style="display:flex;"><span>- Include mocks
</span></span><span style="display:flex;"><span>- Ensure &gt;80% coverage
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Output:
</span></span><span style="display:flex;"><span>- Test file
</span></span><span style="display:flex;"><span>- Coverage explanation
</span></span></code></pre></div><p>优势：</p>
<ul>
<li>输出结构稳定</li>
<li>测试风格统一</li>
<li>可团队复用</li>
</ul>
<h3 id="示例-3api-文档生成-skill">示例 3：API 文档生成 Skill</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Act as an API documentation generator.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Given code:
</span></span><span style="display:flex;"><span>- Extract endpoints
</span></span><span style="display:flex;"><span>- Generate OpenAPI spec
</span></span><span style="display:flex;"><span>- Add request/response examples
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Output in YAML
</span></span></code></pre></div><p>直接产出可用 API 文档</p>
<h3 id="示例-4代码评审-skill强烈推荐">示例 4：代码评审 Skill（强烈推荐）</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Act as a senior code reviewer.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Review this code:
</span></span><span style="display:flex;"><span>- Check performance issues
</span></span><span style="display:flex;"><span>- Identify security risks
</span></span><span style="display:flex;"><span>- Suggest improvements
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Output:
</span></span><span style="display:flex;"><span>- Issues list
</span></span><span style="display:flex;"><span>- Severity level
</span></span><span style="display:flex;"><span>- Fix suggestions
</span></span></code></pre></div><p>这个 Skill 在团队中极其高价值</p>
<h2 id="进阶skill-组合skill-pipeline">进阶：Skill 组合（Skill Pipeline）</h2>
<p>真正强大的不是单个 Skill，而是<strong>Skill 组合</strong>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>需求 → API Skill → 实现 Skill → 测试 Skill → Review Skill
</span></span></code></pre></div><p>形成一个 AI 开发流水线：</p>
<ol>
<li>生成 API</li>
<li>生成实现</li>
<li>生成测试</li>
<li>自动 review</li>
</ol>
<p>本质：AI 驱动的 Dev Pipeline</p>
<h2 id="最佳实践">最佳实践</h2>
<h3 id="1-skill-必须结构化">1. Skill 必须结构化</h3>
<p>不要写：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>帮我优化代码
</span></span></code></pre></div><p>要写：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Optimize code:
</span></span><span style="display:flex;"><span>- Reduce time complexity
</span></span><span style="display:flex;"><span>- Improve readability
</span></span><span style="display:flex;"><span>- Add comments
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Output:
</span></span><span style="display:flex;"><span>- Before/After
</span></span><span style="display:flex;"><span>- Explanation
</span></span></code></pre></div><h3 id="2-强制输出格式">2. 强制输出格式</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Output in JSON:
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  &#34;issues&#34;: [],
</span></span><span style="display:flex;"><span>  &#34;fixes&#34;: []
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>这对自动化极其重要</p>
<h3 id="3-限定角色role-prompting">3. 限定角色（Role Prompting）</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Act as:
</span></span><span style="display:flex;"><span>- Staff Engineer
</span></span><span style="display:flex;"><span>- Security Expert
</span></span><span style="display:flex;"><span>- Performance Engineer
</span></span></code></pre></div><h3 id="4-skill-应该可复制">4. Skill 应该可复制</h3>
<p>建议统一放在：</p>
<ul>
<li><code>.github/copilot-instructions.md</code></li>
<li><code>docs/ai-skills.md</code></li>
<li>内部 wiki</li>
</ul>
<h2 id="skill-vs-agent很多人会混淆">Skill vs Agent（很多人会混淆）</h2>
<table>
	<thead>
			<tr>
					<th>概念</th>
					<th>本质</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Skill</td>
					<td>单一能力</td>
			</tr>
			<tr>
					<td>Agent</td>
					<td>多 Skill + 自动决策</td>
			</tr>
	</tbody>
</table>
<p>Skill 是积木
Agent 是机器人</p>
<h2 id="总结">总结</h2>
<p>AI 编程正在从：</p>

<blockquote>
  <p>“怎么问 AI” → “怎么设计 AI 能力”</p>

</blockquote>
<p>Skill 的出现意味着：</p>
<ul>
<li>Prompt 不再是核心</li>
<li><strong>能力设计才是核心</strong></li>
<li>AI 正在变成“开发基础设施”</li>
</ul>

<blockquote>
  <p>未来优秀的工程师，不只是写代码的人，而是<strong>设计 AI 能力的人</strong></p>

</blockquote>
]]></content:encoded></item><item><title>Clawdbot：让您在 Azure 上拥有一个强大的 AI 全能助手</title><link>https://wilsonwu.me/zh/blog/2026/clawdbot-on-azure/</link><pubDate>Sun, 25 Jan 2026 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2026/clawdbot-on-azure/</guid><description>&lt;p&gt;&lt;img alt="Clawdbot" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/clawdbot-on-azure/1-clawdbot.png"&gt;&lt;/p&gt;
&lt;p&gt;随着生成式 AI 与智能代理的快速发展，自托管的智能系统正变得越来越受开发者和技术团队关注。&lt;strong&gt;Clawdbot&lt;/strong&gt; 就是这样一款开源、可自托管的个人 AI 助手，它不仅能与用户进行对话，还能执行任务、集成消息平台、提供自动化能力，并可部署在云平台上如 Azure。本文将逐步介绍 Clawdbot 的能力、架构以及如何在 &lt;strong&gt;Microsoft Azure&lt;/strong&gt; 上部署并开始使用它。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="Clawdbot" loading="lazy" src="/zh/blog/2026/clawdbot-on-azure/1-clawdbot.png"></p>
<p>随着生成式 AI 与智能代理的快速发展，自托管的智能系统正变得越来越受开发者和技术团队关注。<strong>Clawdbot</strong> 就是这样一款开源、可自托管的个人 AI 助手，它不仅能与用户进行对话，还能执行任务、集成消息平台、提供自动化能力，并可部署在云平台上如 Azure。本文将逐步介绍 Clawdbot 的能力、架构以及如何在 <strong>Microsoft Azure</strong> 上部署并开始使用它。</p>
<h2 id="什么是-clawdbot">什么是 Clawdbot</h2>
<p><strong>Clawdbot</strong> 是一个开源的个人 AI 助手项目，本质上是一个智能代理框架，它可以：</p>
<ul>
<li>与多种消息平台进行交互，包括 WhatsApp、Telegram、Discord、Slack、Microsoft Teams 等。</li>
<li>承担智能对话、任务执行、自动化工作流的角色，而非单纯聊天机器人。</li>
<li>支持在本地或自托管服务器上运行，数据默认保存在本地，增强隐私保护。</li>
<li>通过插件/技能扩展，实现日程提醒、文件管理、邮件处理、代码生成等功能。</li>
</ul>
<p>Clawdbot 的核心由 <strong>Gateway、Providers（平台接入）、Agents/Skills（智能任务执行单元）</strong>构成。Gateway 负责消息总线、Providers 负责与各消息平台对接，Agents/Skills 则实际执行用户请求。</p>
<h2 id="clawdbot-的主要特性">Clawdbot 的主要特性</h2>
<ol>
<li>
<p><strong>多平台消息接入</strong>
Clawdbot 支持通过连接聊天平台 API，接收来自 WhatsApp、Telegram、Discord、Signal、Slack、Microsoft Teams 等消息，并将这些消息传递给 AI 模型进行处理。</p>
</li>
<li>
<p><strong>隐私和数据控制</strong>
与云端托管的聊天 AI 不同，Clawdbot 可以运行在你自己的服务器或 VPS 上，所有对话历史、配置、凭据都可以按需存储和访问。</p>
</li>
<li>
<p><strong>自动化执行能力</strong>
除了对话，Clawdbot 还能执行具体任务，包括浏览器控制、文件操作、数据处理、发送提醒等，这使其成为一个更为复杂的个人助手平台。</p>
</li>
<li>
<p><strong>可扩展的模型支持</strong>
Clawdbot 支持多种 AI 模型提供者（如 OpenAI GPT、Anthropic Claude，甚至本地 LLM 服务如 Ollama）。</p>
</li>
</ol>
<h2 id="在-azure-vm-部署-clawdbot">在 Azure VM 部署 Clawdbot</h2>
<p>虽然 Clawdbot 支持本地运行或容器平台，但在以下场景中，<strong>Azure VM 是非常合适的部署形态</strong>：</p>
<ol>
<li>
<p><strong>完全控制运行环境</strong></p>
<ul>
<li>自由选择 OS（Ubuntu / Debian）</li>
<li>自由安装 Node.js、Docker、GPU 驱动等</li>
</ul>
</li>
<li>
<p><strong>数据与隐私可控</strong></p>
<ul>
<li>所有对话记录、日志、配置文件仅存在于 VM 内</li>
<li>不依赖第三方 PaaS</li>
</ul>
</li>
<li>
<p><strong>适合长期运行的 Agent</strong></p>
<ul>
<li>7×24 小时在线</li>
<li>可通过 systemd 或 Docker 保证自动重启</li>
</ul>
</li>
<li>
<p><strong>可扩展性良好</strong></p>
<ul>
<li>可横向扩容（多 VM + 负载均衡）</li>
<li>可纵向扩容（更大规格 CPU / GPU）</li>
</ul>
</li>
</ol>
<h2 id="整体部署架构概览">整体部署架构概览</h2>
<p>在 Azure VM 上运行 Clawdbot 的典型架构如下：</p>
<p><img alt="架构" loading="lazy" src="/zh/blog/2026/clawdbot-on-azure/2-architecture.png"></p>
<h2 id="创建-azure-vm">创建 Azure VM</h2>
<h3 id="vm-基础配置建议">VM 基础配置建议</h3>
<ul>
<li><strong>OS</strong>：Ubuntu 22.04 LTS</li>
<li><strong>规格</strong>：
<ul>
<li>轻量使用：Standard B2s / B4ms</li>
<li>多 Agent：D4s_v5 及以上</li>
</ul>
</li>
<li><strong>磁盘</strong>：至少 64 GB</li>
<li><strong>网络</strong>：
<ul>
<li>入站：22（SSH）</li>
<li>入站：3000（或自定义 Clawdbot 端口）</li>
</ul>
</li>
</ul>
<h3 id="登录-vm">登录 VM</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh azureuser@&lt;VM_PUBLIC_IP&gt;
</span></span></code></pre></div><h2 id="安装运行时环境">安装运行时环境</h2>
<h3 id="系统依赖">系统依赖</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt update <span style="color:#f92672">&amp;&amp;</span> sudo apt upgrade -y
</span></span><span style="display:flex;"><span>sudo apt install -y git curl build-essential
</span></span></code></pre></div><h3 id="安装-nodejs推荐-20-lts">安装 Node.js（推荐 20 LTS）</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
</span></span><span style="display:flex;"><span>sudo apt install -y nodejs
</span></span><span style="display:flex;"><span>node -v
</span></span><span style="display:flex;"><span>npm -v
</span></span></code></pre></div><h2 id="安装-clawdbot">安装 Clawdbot</h2>
<h3 id="全局-cli--本地运行目录">全局 CLI + 本地运行目录</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo npm install -g clawdbot
</span></span></code></pre></div><p>创建运行目录：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir ~/clawdbot
</span></span><span style="display:flex;"><span>cd ~/clawdbot
</span></span></code></pre></div><p>初始化配置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>clawdbot onboard
</span></span></code></pre></div><p>跟着步骤完成各类配置即可：</p>
<p><img alt="配置" loading="lazy" src="/zh/blog/2026/clawdbot-on-azure/3-onboard.png"></p>
<h2 id="启动-clawdbot">启动 Clawdbot</h2>
<h3 id="手动启动验证">手动启动验证</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>clawdbot gateway
</span></span></code></pre></div><p>确认日志中出现：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>🦞 Clawdbot 2026.1.24-3 (885167d) — I speak fluent bash, mild sarcasm, and aggressive tab-completion energy.
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>16:18:27 [heartbeat] started
</span></span><span style="display:flex;"><span>16:18:27 [gateway] agent model: openai-codex/gpt-5.1
</span></span><span style="display:flex;"><span>16:18:27 [gateway] listening on ws://127.0.0.1:18789 (PID 36888)
</span></span><span style="display:flex;"><span>16:18:27 [gateway] listening on ws://[::1]:18789
</span></span><span style="display:flex;"><span>16:18:27 [gateway] log file: \tmp\clawdbot\clawdbot-2026-01-27.log
</span></span><span style="display:flex;"><span>16:18:27 [browser/server] Browser control listening on http://127.0.0.1:18791/
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><p>通过浏览器打开：<a href="http://127.0.0.1:18789/">http://127.0.0.1:18789/</a></p>
<p><img alt="界面" loading="lazy" src="/zh/blog/2026/clawdbot-on-azure/4-interface.png"></p>
<p>即可开始使用！</p>
<h2 id="结语">结语</h2>
<p>Clawdbot 是一款功能强大的开源 AI 个人助手，可将 AI 交互扩展到多种消息平台，并集成丰富的执行能力。通过将 Clawdbot 部署到 Azure，你可以将它打造成一个企业级、自托管的 AI 服务，提高可用性、安全性和可扩展性。上述步骤覆盖了从创建 Azure Bot 到完整部署 Clawdbot 的全流程，希望能帮助你快速搭建并运行自己的智能助理。</p>
]]></content:encoded></item><item><title>微软 TRELLIS 大语言模型：面向生产级 3D 资产生成的大模型及基于 Azure 部署使用指南</title><link>https://wilsonwu.me/zh/blog/2026/llm-microsoft-trellis-3d/</link><pubDate>Mon, 19 Jan 2026 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2026/llm-microsoft-trellis-3d/</guid><description>&lt;p&gt;&lt;img alt="Trellis-3D" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/llm-microsoft-trellis-3d/1-trellis-3d.png"&gt;&lt;/p&gt;
&lt;p&gt;2025 年末，微软研究院发布了一个面向 3D 内容创作的开源大模型项目 &lt;strong&gt;TRELLIS&lt;/strong&gt;，并伴随学术论文&lt;a href="https://arxiv.org/abs/2412.01506"&gt;《Structured 3D Latents for Scalable and Versatile 3D Generation》&lt;/a&gt;。该项目通过统一的结构化潜在空间与先进的流模型技术，显著提升了文本/图像到 3D 资产生成的质量与灵活性，同时拓展了 3D 模型的多格式输出与编辑能力，成为当前 3D AI 模型生态中的重要技术之一。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="Trellis-3D" loading="lazy" src="/zh/blog/2026/llm-microsoft-trellis-3d/1-trellis-3d.png"></p>
<p>2025 年末，微软研究院发布了一个面向 3D 内容创作的开源大模型项目 <strong>TRELLIS</strong>，并伴随学术论文<a href="https://arxiv.org/abs/2412.01506">《Structured 3D Latents for Scalable and Versatile 3D Generation》</a>。该项目通过统一的结构化潜在空间与先进的流模型技术，显著提升了文本/图像到 3D 资产生成的质量与灵活性，同时拓展了 3D 模型的多格式输出与编辑能力，成为当前 3D AI 模型生态中的重要技术之一。</p>
<p>官方 Github 仓库地址：<a href="https://github.com/microsoft/TRELLIS">https://github.com/microsoft/TRELLIS</a></p>
<h2 id="trellis-是什么核心概念与架构">TRELLIS 是什么？——核心概念与架构</h2>
<p>TRELLIS 是微软构建的<strong>大型 3D 资产生成模型</strong>，支持输入文本提示或图像，并输出高质量的三维模型资产。其技术创新点主要包括：</p>
<ul>
<li><strong>统一结构化潜在表示（SLAT）</strong>：将三维信息编码为一种可扩展的结构化潜在空间表示，使模型能够以统一方式理解和生成不同表现形式的 3D 结果。</li>
<li><strong>整流流（Rectified Flow Transformers）</strong>：专为 SLAT 设计的生成骨干网络，通过适应 sparse 表示提升生成效率与质量。</li>
<li><strong>大规模训练与预训练模型</strong>：模型规模最高达<strong>约 20 亿参数</strong>，并在包含 <strong>50 万多样化三维资产的数据集</strong>上训练，具备强泛化能力。</li>
</ul>
<p>TRELLIS 不仅能生成三维几何，还能捕获复杂纹理与外观信息，使得生成的资产更贴近真实世界中可用的三维内容。</p>
<h2 id="主要功能特性">主要功能特性</h2>
<p>以下是 TRELLIS 的核心功能模块及技术亮点：</p>
<h3 id="多模态输入能力">多模态输入能力</h3>
<p>TRELLIS 支持以下输入条件：</p>
<ul>
<li><strong>文本提示（Text-to-3D）</strong>：通过自然语言描述生成三维资产（提示须清晰准确）。</li>
<li><strong>图像条件（Image-to-3D）</strong>：根据一张或多张图片生成对应三维模型。</li>
</ul>
<p>这种多模态输入支持，使 TRELLIS 适用于从概念设计到真实原型的全流程创作。</p>
<p><img alt="3D" loading="lazy" src="/zh/blog/2026/llm-microsoft-trellis-3d/2-to-3d.png"></p>
<h3 id="灵活的输出格式">灵活的输出格式</h3>
<p>根据下游需求，TRELLIS 输出包括：</p>
<ul>
<li><strong>辐射场（NeRF/Radiance Fields）</strong>：适合渲染与展示</li>
<li><strong>3D 高斯表示</strong>：一种可渲染密度表示</li>
<li><strong>传统网格 Mesh</strong>：可导出为标准 3D 文件格式（例如 GLB/OBJ）用于游戏、AR/VR 等应用</li>
</ul>
<p>这种格式灵活性是模型在实际生产环境中落地的关键能力。</p>
<h3 id="本地三维编辑与变体生成">本地三维编辑与变体生成</h3>
<p>与早期单一生成模型不同，TRELLIS 能：</p>
<ul>
<li><strong>局部编辑</strong>：在已有模型上进行细部修改</li>
<li><strong>生成变体</strong>：在保持基本结构不变的前提下，创建风格、细节不同的多版本输出</li>
</ul>
<p>这种能力特别适合迭代式 3D 内容设计。</p>
<h3 id="支持大规模数据集训练与可扩展性">支持大规模数据集训练与可扩展性</h3>
<p>TRELLIS 在包含多种来源（如 Objaverse、ABO 等）的大规模 3D 数据集上进行训练，使其具备：</p>
<ul>
<li><strong>强泛化能力</strong></li>
<li><strong>更丰富的几何与纹理表现</strong></li>
<li><strong>多任务适配能力</strong></li>
</ul>
<p>这样的数据基础是其较竞品更具优势的根本原因之一。</p>
<h2 id="与其他竞品对比优势与提升点">与其他竞品对比：优势与提升点</h2>
<p>目前市场上其他比较知名的 3D 生成模型（例如 <strong>OpenAI Shap-E</strong>、或研究型模型如 MeshGen 等）多存在以下局限：</p>
<ul>
<li>有的只能输出单一 3D 表示（如内隐函数或点云）</li>
<li>多数不支持本地编辑与多种格式输出</li>
<li>在复杂纹理、细节表现上效果有限</li>
</ul>
<p>相比之下：</p>
<h3 id="trellis-的主要优势包括">TRELLIS 的主要优势包括</h3>
<ol>
<li><strong>结构化潜在表示（SLAT）框架</strong>：支持统一的生成与解码机制，同时兼顾几何与纹理细节。</li>
<li><strong>多格式生成输出</strong>：与只生成单种数据结构的竞品相比更通用。</li>
<li><strong>本地编辑与变体生成</strong>：提升了设计迭代效率。</li>
<li><strong>大规模预训练与更丰富数据集支撑</strong>：提升了模型对稀有或复杂对象的生成能力。</li>
</ol>
<p>总的来说，TRELLIS 更偏向工业级 3D 生成工具，而不只是科研展示或单一生成模型。</p>
<h2 id="部署和使用">部署和使用</h2>
<p>目前，微软尚未在 <strong>Azure AI Foundry / Microsoft Foundry</strong> 中以 Serverless 或托管模型的形式直接提供 TRELLIS。因此，在 Azure 云环境中使用 TRELLIS 的<strong>最可行、也是最稳定的方式</strong>，是基于 <strong>Azure GPU 虚拟机（VM）</strong>进行自托管部署。</p>
<p>这种方式具备以下优势：</p>
<ul>
<li>完全控制模型版本与推理流程</li>
<li>支持大显存 GPU，适合 3D 模型生成场景</li>
<li>可与企业现有 Azure 网络、安全、存储体系无缝集成</li>
</ul>
<h3 id="1-azure-gpu-vm-规格选型建议">1. Azure GPU VM 规格选型建议</h3>
<p>TRELLIS 属于<strong>显存与算力敏感型 3D 生成模型</strong>，对 GPU 资源要求较高，推荐如下规格：</p>
<h4 id="推荐-vm-系列">推荐 VM 系列</h4>
<ul>
<li><strong>Standard_NC A100 v4</strong>（A100 80GB，首选）</li>
<li><strong>Standard_NC A100 v5</strong>（A100 80GB，新一代）</li>
<li><strong>Standard_ND H100 v5</strong>（H100，若需要更高吞吐）</li>
</ul>

<blockquote>
  <p>实践建议：</p>
<ul>
<li><strong>推理 / Demo</strong>：单卡 A100 80GB 即可</li>
<li><strong>高分辨率 / 批量生成 / 编辑任务</strong>：建议单机多卡或更高规格</li>
</ul>

</blockquote>
<h3 id="2-操作系统与基础环境">2. 操作系统与基础环境</h3>
<p>推荐环境组合如下：</p>
<ul>
<li>OS：Ubuntu 20.04 / 22.04 LTS</li>
<li>CUDA：与 VM 驱动匹配的官方版本</li>
<li>Python：3.8 或以上</li>
<li>PyTorch：官方 CUDA 版本</li>
</ul>
<p>Azure Marketplace 中可直接选择 <strong>CUDA + NVIDIA Driver 预装镜像</strong>，可显著降低环境准备成本。</p>
<h3 id="3-trellis-安装与依赖配置">3. TRELLIS 安装与依赖配置</h3>
<h4 id="拉取代码">拉取代码</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone --recurse-submodules https://github.com/microsoft/TRELLIS.git
</span></span><span style="display:flex;"><span>cd TRELLIS
</span></span></code></pre></div><h4 id="创建独立-python-环境并安装依赖">创建独立 Python 环境并安装依赖</h4>
<p>官方已提供自动化脚本：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./setup.sh --new-env --basic
</span></span></code></pre></div><p>如需支持训练、编辑或高级功能，可根据官方说明启用对应参数。</p>
<h3 id="4-基于-azure-vm-的推理运行方式">4. 基于 Azure VM 的推理运行方式</h3>
<h4 id="方式一命令行推理适合批量任务">方式一：命令行推理（适合批量任务）</h4>
<p>文本或图像驱动生成 3D 模型：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python example_text.py <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --prompt <span style="color:#e6db74">&#34;a futuristic electric motorcycle&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --output_dir outputs/
</span></span></code></pre></div><p>或多图像条件输入：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python example_multi_image.py <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --input_images images/ <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --output_dir outputs/
</span></span></code></pre></div><p>生成结果可直接导出为 Mesh / Radiance Field 等格式，用于后续渲染或工具链集成。</p>
<h4 id="方式二gradio-web-ui推荐用于演示与验证">方式二：Gradio Web UI（推荐用于演示与验证）</h4>
<p>TRELLIS 官方提供基于 Gradio 的 Web Demo，适合：</p>
<ul>
<li>团队内部体验</li>
<li>产品验证</li>
<li>算法效果对比</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python app.py
</span></span></code></pre></div><p>然后通过 Azure VM 公网 IP 或内网负载均衡访问 Web UI。</p>
<h3 id="5-azure-上的工程化部署建议">5. Azure 上的工程化部署建议</h3>
<p>为了更好地在 Azure 上长期运行 TRELLIS，建议采用以下架构实践：</p>
<h4 id="存储解耦">存储解耦</h4>
<ul>
<li>模型权重：Azure Blob Storage</li>
<li>生成结果：Blob / Azure Files</li>
<li>日志与元数据：Azure Monitor / Log Analytics</li>
</ul>
<h4 id="服务化封装">服务化封装</h4>
<ul>
<li>使用 <strong>FastAPI / Flask</strong> 将 TRELLIS 推理封装为 HTTP API</li>
<li>GPU VM 作为推理节点</li>
<li>前端或工作流通过 REST 调用</li>
</ul>
<h4 id="安全与网络">安全与网络</h4>
<ul>
<li>VM 置于 VNet 内</li>
<li>Web UI / API 通过 Application Gateway 或内部访问</li>
<li>支持私有部署，满足企业安全要求</li>
</ul>
<h2 id="总结与展望">总结与展望</h2>
<p>微软 TRELLIS 代表了当前 3D AI 生成技术的重要进步，它在生成质量、格式多样性与本地编辑能力方面具备显著优势。对于希望自动化 3D 内容创建的开发者与设计者而言，它是一个有实力的开源方案。部署方面，虽然目前官方尚未将其作为 Foundry 的 serverless 模型，但可通过 Azure 的常规模型部署管道与 GPU 计算能力在云端高效运行 TRELLIS 推理与自定义训练。</p>
]]></content:encoded></item><item><title>图像生成进入平台时代：GPT-Image-1.5 在 Microsoft Foundry 中的应用</title><link>https://wilsonwu.me/zh/blog/2025/azure-openai-gpt-image/</link><pubDate>Wed, 17 Dec 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/azure-openai-gpt-image/</guid><description>&lt;p&gt;&lt;img alt="GPT-Image-1.5" loading="lazy" src="https://wilsonwu.me/zh/blog/2025/azure-openai-gpt-image/0-gpt-image-1.5.png"&gt;&lt;/p&gt;
&lt;p&gt;近年来，生成式 AI 技术迅速演进。在自然语言处理之外，&lt;strong&gt;图像生成与编辑能力成为 AI 创新的重要前沿&lt;/strong&gt;。在这一趋势下，OpenAI 推出了 GPT Image 系列模型，在 Azure OpenAI 服务内同样可用。而其中最新发布的 &lt;strong&gt;GPT-Image-1.5&lt;/strong&gt; 可视为图像生成领域的新旗舰，在性能、效率和可控性方面均有显著提升。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="GPT-Image-1.5" loading="lazy" src="/zh/blog/2025/azure-openai-gpt-image/0-gpt-image-1.5.png"></p>
<p>近年来，生成式 AI 技术迅速演进。在自然语言处理之外，<strong>图像生成与编辑能力成为 AI 创新的重要前沿</strong>。在这一趋势下，OpenAI 推出了 GPT Image 系列模型，在 Azure OpenAI 服务内同样可用。而其中最新发布的 <strong>GPT-Image-1.5</strong> 可视为图像生成领域的新旗舰，在性能、效率和可控性方面均有显著提升。</p>
<h2 id="什么是-gpt-image-15">什么是 GPT-Image-1.5</h2>
<p>GPT-Image-1.5 是 OpenAI 最新发布的<strong>多模态图像生成模型</strong>，属于 GPT Image 系列，目前是效果最佳的版本。与前代（如 GPT-Image-1）相比，它在指令遵循能力、图像质量、生成速度和成本效率方面都有明显提升。</p>
<p>官方定义上，GPT Image 模型是<strong>原生多模态语言模型（natively multimodal），能接收文本和图像输入，并生成图像输出</strong>。在功能上，它不仅支持从纯文本生成图像，还可以对已有图像进行编辑、修改、风格化转换等操作。</p>
<p>简而言之：</p>
<ul>
<li><strong>多模态</strong>：同时理解文本和图像输入。</li>
<li><strong>图像生成与编辑</strong>：支持从文本生成全新图像，也支持对已有图像进行局部修改和增强。</li>
<li><strong>高质量与高效</strong>：相比前代生成更精细、更快速，同时成本更低。</li>
</ul>
<h2 id="核心能力与特点">核心能力与特点</h2>
<h3 id="1-指令遵循及表达精准">1. 指令遵循及表达精准</h3>
<p>GPT-Image-1.5 在理解自然语言提示方面得到显著提升，尤其擅长：</p>
<ul>
<li><strong>对复杂描述的视觉表达</strong>，细节理解更准确</li>
<li><strong>在图像中生成清晰可读的文本内容</strong></li>
<li><strong>遵循用户指令进行定制化修改和组合</strong></li>
</ul>
<p>相比最初的图像生成模型（例如 DALL·E 系列），这类 GPT Image 系列模型对于提示语的响应更直观、可控性更高。</p>
<h3 id="2-图像质量与生成速度提升">2. 图像质量与生成速度提升</h3>
<p>GPT-Image-1.5 的生成速度比上一代快得多（据报道最高可达大约 4 倍提升），这对于需要快速迭代视觉内容的场景（如设计、产品原型等）尤为重要。</p>
<p>此外，图像生成结果在细节、现实感和一致性方面表现更好，尤其是在脸部、纹理、光影等关键元素上有显著进步。</p>
<h3 id="3-编辑与增强功能">3. 编辑与增强功能</h3>
<p>GPT-Image-1.5 同时支持图像编辑，包括：</p>
<ul>
<li><strong>局部修改</strong>：对选中区域进行变更</li>
<li><strong>添加或移除元素</strong></li>
<li><strong>风格化调整与概念转换</strong></li>
</ul>
<p>无需完全重新生成整幅图像，大幅提升工作流效率。</p>
<h3 id="4-成本与效率优化">4. 成本与效率优化</h3>
<p>GPT-Image-1.5 在 API 调用成本上较前代降低约 20%，使得高质量图像生成在更大范围内可持续使用，特别适合企业级批量生成需求。</p>
<h3 id="5-安全性与合规性">5. 安全性与合规性</h3>
<p>作为 Azure OpenAI 服务的一部分，GPT-Image-1.5 的使用受 Azure 的<strong>安全性、合规性和内容过滤体系</strong>保障，支持多种定制化审核与责任使用防护。</p>
<h2 id="在-microsoft-foundry-中部署与使用-gpt-image-15">在 Microsoft Foundry 中部署与使用 GPT-Image-1.5</h2>
<p>随着 <strong>Microsoft Foundry</strong> 的推出（当前为预览版），微软正在将 Azure OpenAI、模型目录、推理端点、Playground 与企业治理能力整合为<strong>统一的 AI 应用构建平台</strong>。在该预览版中，<strong>GPT-Image-1.5 已作为一等模型（first-class model）被直接纳入 Foundry 的模型体系</strong>，无需再通过传统的 “Azure AI Foundry / Azure AI Studio” 路径进行管理。</p>
<h3 id="1-在-microsoft-foundry-中部署-gpt-image-15">1. 在 Microsoft Foundry 中部署 GPT-Image-1.5</h3>
<p>在 Preview 版 Foundry 中，模型部署流程更接近“产品级模型托管”而非传统 Azure 资源配置：</p>
<p>首先进入 <strong>Microsoft Foundry（Preview）控制台</strong>。在左侧选择 <strong>Models</strong>（模型库）。在模型列表中找到 <strong>GPT-Image-1.5</strong>（Image / Multimodal 分类）。</p>
<p><img alt="GPT-Image-1.5" loading="lazy" src="/zh/blog/2025/azure-openai-gpt-image/1-gpt-image-1.5.png"></p>
<p>打开模型详情页后，点击 <strong>Deploy</strong>，创建一个新的推理部署。</p>
<p><img alt="GPT-Image-1.5 详情" loading="lazy" src="/zh/blog/2025/azure-openai-gpt-image/2-gpt-image-1.5-detail.png"></p>
<p>选择默认部署，简单选择 <strong>Region</strong> 后即可完成部署，整个过程非常简单：</p>
<p><img alt="选择区域" loading="lazy" src="/zh/blog/2025/azure-openai-gpt-image/3-choose-region.png"></p>
<p>部署完成后会自动进入 GPT-Image-1.5 的 Playground：</p>
<p><img alt="进入 Playground" loading="lazy" src="/zh/blog/2025/azure-openai-gpt-image/4-playground.png"></p>
<p>完成后，Foundry 会自动生成：</p>
<ul>
<li>一个可调用的 <strong>Inference Endpoint</strong></li>
<li>与之绑定的身份、配额与监控策略</li>
</ul>
<p>这一流程强调的是<strong>模型即服务（Model as a Service）</strong>，开发者不再需要直接关心底层 Azure OpenAI 资源的复杂配置。</p>
<h3 id="2-使用-foundry-playground-进行图像生成与-prompt-验证">2. 使用 Foundry Playground 进行图像生成与 Prompt 验证</h3>
<p>Microsoft Foundry（Preview）内置了统一的 <strong>Playground</strong>，用于对 GPT-Image-1.5 进行交互式试验。</p>
<p>在 Playground 中，你可以：</p>
<ul>
<li>直接输入文本 Prompt 生成图像</li>
<li>调整图像尺寸、质量等级等参数</li>
<li>快速验证复杂描述、风格化指令的效果</li>
<li>对比不同 Prompt 的生成结果</li>
</ul>
<p>这一能力特别适合以下角色协作使用：</p>
<ul>
<li>产品经理：验证视觉创意可行性</li>
<li>设计师：探索生成风格与构图</li>
<li>工程师：在写代码前固化 Prompt 模板</li>
</ul>
<p>Playground 在 Preview 版 Foundry 中的定位是 <strong>Prompt 即资产</strong>，验证通过的 Prompt 可以直接迁移到生产代码中使用。</p>
<p>生成图片后效果如下：</p>
<p><img alt="生成图片" loading="lazy" src="/zh/blog/2025/azure-openai-gpt-image/5-gen.png"></p>
<h3 id="3-从-foundry-到应用代码的集成方式">3. 从 Foundry 到应用代码的集成方式</h3>
<p>在应用层面，<strong>Microsoft Foundry（Preview）部署的 GPT-Image-1.5 本质上仍然以标准推理 Endpoint 的形式对外提供服务</strong>：</p>
<ul>
<li>
<p>代码侧只需要关心：</p>
<ul>
<li>Endpoint URL</li>
<li>Deployment Name</li>
<li>身份认证方式</li>
</ul>
</li>
<li>
<p>不再直接依赖模型原始名称或版本号</p>
</li>
</ul>
<p>在企业实践中，常见模式是：</p>
<ul>
<li>开发 / 测试 / 生产环境使用不同 Deployment</li>
<li>模型版本升级（例如 GPT-Image-1.5 → 后续版本）在 Foundry 层完成</li>
<li>应用代码保持不变，仅消费稳定的 Endpoint</li>
</ul>
<p>这种模式显著降低了 <strong>模型演进对业务系统的侵入性</strong>。</p>
<h2 id="在-azure-openai-中如何使用-gpt-image-15">在 Azure OpenAI 中如何使用 GPT-Image-1.5</h2>
<p>你可以通过 Azure OpenAI 提供的 API、SDK 或 Azure AI Foundry 平台来调用 GPT-Image-1.5。基本流程如下：</p>
<h3 id="1-获取访问权限与部署模型">1. 获取访问权限与部署模型</h3>
<p>在模型的详情页可以查看通过代码嗲用 GPT-Image-1.5 的所有信息：</p>
<p><img alt="模型详情" loading="lazy" src="/zh/blog/2025/azure-openai-gpt-image/6-model-detail.png"></p>
<h3 id="2-使用-rest-api-生成图像">2. 使用 REST API 生成图像</h3>
<p>使用 <code>POST</code> 请求调用图像生成端点：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>POST https://&lt;your_resource&gt;.openai.azure.com/openai/deployments/&lt;your_deployment&gt;/images/generations?api-version<span style="color:#f92672">=</span>&lt;api_version&gt;
</span></span><span style="display:flex;"><span>Content-Type: application/json
</span></span><span style="display:flex;"><span>api-key: &lt;your_api_key&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;prompt&#34;</span>: <span style="color:#e6db74">&#34;一位穿宇航服的猫在火星表面散步&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;size&#34;</span>: <span style="color:#e6db74">&#34;1024x1024&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;n&#34;</span>: 1,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;quality&#34;</span>: <span style="color:#e6db74">&#34;high&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>常见参数说明：</p>
<ul>
<li><code>prompt</code>: 文本描述</li>
<li><code>size</code>: 图像尺寸（如 1024x1024）</li>
<li><code>n</code>: 生成图像数量</li>
<li><code>quality</code>: 生成质量（low/medium/high）</li>
</ul>
<h3 id="3-图像编辑若支持">3. 图像编辑（若支持）</h3>
<p>上传已有图片并通过 prompt 指令结合修改：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>POST /images/edits
</span></span></code></pre></div><p>此类调用可以指定 mask、输入图片等字段来控制局部变化。</p>
<h3 id="4-sdk-与开发环境">4. SDK 与开发环境</h3>
<p>Azure 支持多种语言 SDK（例如 Python、C#），可借助 Azure Identity 认证库、安全地调用 API 并处理生成结果。</p>
<h2 id="总结">总结</h2>
<p>GPT-Image-1.5 在图像生成领域代表了下一阶段 AI 创意工具的能力边界。它在指令理解、生成速度、图像质量、成本效率与多模态处理方面均显著提升，为开发者和企业提供了一套强大且可扩展的视觉 AI 解决方案。</p>
<p>对于希望构建中长期 AI 能力平台的团队而言，Foundry + GPT-Image-1.5 是比直接裸用 API 更具工程化和可持续性的选择。</p>
<p>Azure OpenAI 服务结合了模型能力与企业级安全合规，让 GPT-Image-1.5 不仅适用于实验探索，也能支撑生产级应用。</p>
<p>如果你正在构建需要视觉理解或自动创意生成的应用，无论是批量生成还是交互式体验，GPT-Image-1.5 都值得深入实践。</p>
]]></content:encoded></item><item><title>基于 Azure OpenAI 构建智能地址解析 Chrome / Edge 浏览器扩展插件</title><link>https://wilsonwu.me/zh/blog/2025/edge-extension-with-azure-ai/</link><pubDate>Mon, 15 Dec 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/edge-extension-with-azure-ai/</guid><description>&lt;p&gt;在电商和物流领域，有一个不起眼但极其耗时的痛点：&lt;strong&gt;将复杂的地址格式正确的输入到物流系统固定的字段中&lt;/strong&gt;。客户发来的地址格式千奇百怪，有时是逗号分隔，有时连成一行，顺序也完全随机，经常还缺少省份或州等关键信息。&lt;/p&gt;</description><content:encoded><![CDATA[<p>在电商和物流领域，有一个不起眼但极其耗时的痛点：<strong>将复杂的地址格式正确的输入到物流系统固定的字段中</strong>。客户发来的地址格式千奇百怪，有时是逗号分隔，有时连成一行，顺序也完全随机，经常还缺少省份或州等关键信息。</p>
<p>本文我想分享我是如何构建 <strong>Auto Address</strong> 的，这是一个利用 <strong>Azure OpenAI</strong> 的强大能力来解决这个问题的 Chrome 及 Edge 浏览器扩展插件，其界面和使用方式及其简单。</p>
<p><img alt="主界面" loading="lazy" src="/zh/blog/2025/edge-extension-with-azure-ai/1-main-interface.png"></p>
<h2 id="问题非结构化数据">问题：非结构化数据</h2>
<p>想象一下收到这段文本：</p>

<blockquote>
  <p>&ldquo;张三 13800138000 广东省深圳市南山区科技园南区R2-B三楼&rdquo;</p>

</blockquote>
<p>为了发货，你需要将其拆解为：</p>
<ul>
<li><strong>姓名</strong>: 张三</li>
<li><strong>电话</strong>: 13800138000</li>
<li><strong>地址</strong>: 科技园南区R2-B三楼</li>
<li><strong>城市</strong>: 深圳市</li>
<li><strong>省份</strong>: 广东省</li>
<li><strong>国家</strong>: 中国</li>
</ul>
<p>手动处理成百上千个订单既繁琐又容易出错。正则表达式（Regex）可以处理某些情况，但当格式变化或用户有错别字时，它们往往无能为力。</p>
<h2 id="解决方案llm-作为解析器">解决方案：LLM 作为解析器</h2>
<p><img alt="Auto Address" loading="lazy" src="/zh/blog/2025/edge-extension-with-azure-ai/2-auto-address.png"></p>
<p>像 Azure OpenAI（如：GPT-4 或 GPT-5）这样的大语言模型在理解上下文方面表现出色。它们能区分街道名称和城市名称，不是因为死板的规则，而是因为它们理解地址的<strong>语义</strong>。</p>
<p>我选择 <strong>Azure OpenAI</strong> 进行此项目主要有两个原因：</p>
<ol>
<li><strong>企业级隐私</strong>：发送到 Azure OpenAI 的数据不会用于训练公共模型。</li>
<li><strong>可靠性</strong>：稳定的正常运行时间和性能。</li>
</ol>
<h2 id="技术揭秘它是如何工作的">技术揭秘：它是如何工作的</h2>
<p>插件的核心逻辑出奇地简单。它获取用户的输入，并将其连同一组非常具体的指令发送到 Azure OpenAI API。</p>
<h3 id="1-system-prompt-系统提示词">1. System Prompt (系统提示词)</h3>
<p>“魔法”在于系统提示词。我们需要确切地告诉模型要做什么，更重要的是，如何格式化输出。</p>
<p>这是代码中实际使用的提示词：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;system&#34;</span>, 
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;You are an address parser. Return ONLY a raw JSON object (no markdown code blocks) with keys: name, province, city, address, zip_code, country. If province/state is missing in the input, infer it from the city. Do not separate house number, include it in the address field.&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>这里使用的关键技巧：</strong></p>
<ul>
<li><strong>角色定义</strong>: &ldquo;You are an address parser.&rdquo; (你是一个地址解析器)</li>
<li><strong>输出约束</strong>: &ldquo;Return ONLY a raw JSON object&rdquo;. (仅返回原始 JSON 对象)。这防止模型添加像 &ldquo;这是您的 JSON：&rdquo; 这样的废话。</li>
<li><strong>Schema 定义</strong>: 我们明确列出了我们想要的键 (<code>name</code>, <code>province</code> 等)。</li>
<li><strong>逻辑注入</strong>: &ldquo;If province/state is missing&hellip; infer it.&rdquo; (如果省/州缺失&hellip;推断它)。这将逻辑处理卸载给了 AI。</li>
</ul>
<h3 id="2-api-调用">2. API 调用</h3>
<p>扩展使用标准的 <code>fetch</code> API 与您的 Azure 部署进行通信。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">url</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">baseUrl</span><span style="color:#e6db74">}</span><span style="color:#e6db74">openai/deployments/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">azureDeployment</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/chat/completions?api-version=2024-05-01-preview`</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#a6e22e">url</span>, {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">method</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;POST&#39;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;Content-Type&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;application/json&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;api-key&#39;</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">azureApiKey</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">body</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>({
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">messages</span><span style="color:#f92672">:</span> [ <span style="color:#75715e">/* ... prompt ... */</span> ],
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">temperature</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0.1</span> <span style="color:#75715e">// 以获得一致、确定的结果
</span></span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>我们使用低 <code>temperature</code> (0.1)，因为我们不需要创造力；我们需要精确度。</p>
<h3 id="3-智能省份推断双重检查">3. 智能省份推断（“双重检查”）</h3>
<p>有时，即使是最好的模型也会遗漏细节，或者输入太模糊。一个常见的问题是缺少省/州信息。</p>
<p>我在 <code>popup.js</code> 中实现了一个“回退”机制。如果解析结果有城市但没有省份，扩展程序会发起<strong>第二次定向 API 调用</strong>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> ((<span style="color:#f92672">!</span><span style="color:#a6e22e">addr</span>.<span style="color:#a6e22e">province</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">addr</span>.<span style="color:#a6e22e">province</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;&#39;</span>) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">addr</span>.<span style="color:#a6e22e">city</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ... 二次调用 ...
</span></span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">messages</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;system&#34;</span>, 
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;You are a geography helper. Return ONLY the province/state name for the given city. Do not return JSON, just the name.&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;user&#34;</span>, 
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`Which province/state is the city &#34;</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">addr</span>.<span style="color:#a6e22e">city</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">countryContext</span><span style="color:#e6db74">}</span><span style="color:#e6db74">?`</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>这确保了在用户看到数据之前，数据尽可能完整。</p>
<h2 id="chrome--edge-扩展架构">Chrome / Edge 扩展架构</h2>
<h3 id="manifest-v3-与-side-panel-侧边栏">Manifest V3 与 Side Panel (侧边栏)</h3>
<p>该扩展基于 <strong>Manifest V3</strong> 构建，这是 Chrome / Edge 扩展平台的最新迭代。</p>
<p>我没有使用点击后就会关闭的传统弹出窗口，而是使用了 <strong>Side Panel API</strong> (<code>&quot;permissions&quot;: [&quot;sidePanel&quot;]</code>)。这允许扩展程序停留在浏览器窗口的一侧。这对工作流程至关重要：用户可以在左侧查看发货单页面，同时在右侧查看解析出的地址数据。</p>
<h3 id="隐私优先">隐私优先</h3>
<p>首先使用前需要通过配置界面设置您要使用的 Azure OpenAI 信息：</p>
<p><img alt="配置界面" loading="lazy" src="/zh/blog/2025/edge-extension-with-azure-ai/3-options-interface.png"></p>
<p>主要的设计决策是<strong>零中间服务器</strong>。</p>
<ul>
<li>扩展程序使用 <code>chrome.storage.sync</code> 将您的 API 密钥本地存储在浏览器中。</li>
<li>请求直接从<strong>您的浏览器</strong> -&gt; <strong>Azure OpenAI</strong>。</li>
<li>扩展开发者不收集任何数据。</li>
</ul>
<h2 id="结语">结语</h2>
<p>通过结合 Chrome / Edge 扩展的灵活性和 Azure OpenAI 的智能，我们可以将繁琐的数据录入任务转化为一键操作。</p>
<p>代码已开源。欢迎查看、Fork 并构建您自己的 AI 驱动工具！</p>
<p><strong>GitHub Repo</strong>: <a href="https://github.com/wilsonwu/autoaddr">https://github.com/wilsonwu/autoaddr</a></p>
<p>如果您使用的是 Microsoft Edge 浏览器，可以通过扩展插件市场安装本插件的最新版本：<a href="https://microsoftedge.microsoft.com/addons/detail/auto-address-smart-ship/ieaphpecggbaiineilddheaghcolkbba">Edge 浏览器扩展插件</a></p>
]]></content:encoded></item><item><title>玩转大语言模型：轻松使用 Azure AI Foundry 提供的 Sora 2 生成视频</title><link>https://wilsonwu.me/zh/blog/2025/azure-ai-foundry-sora-2/</link><pubDate>Mon, 10 Nov 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/azure-ai-foundry-sora-2/</guid><description>&lt;p&gt;随着 Azure AI Foundry 开放对 &lt;strong&gt;Sora 2（OpenAI 生成式视频模型）&lt;/strong&gt;的支持，开发者现在可以在企业级合规、可管控的环境中使用顶尖的视频生成能力。本教程将带你从零开始，通过 &lt;strong&gt;Playground&lt;/strong&gt; 和 &lt;strong&gt;Python SDK&lt;/strong&gt; 两种方式调用 Sora 2，完成「文本生成视频」的流程。&lt;/p&gt;</description><content:encoded><![CDATA[<p>随着 Azure AI Foundry 开放对 <strong>Sora 2（OpenAI 生成式视频模型）</strong>的支持，开发者现在可以在企业级合规、可管控的环境中使用顶尖的视频生成能力。本教程将带你从零开始，通过 <strong>Playground</strong> 和 <strong>Python SDK</strong> 两种方式调用 Sora 2，完成「文本生成视频」的流程。</p>
<h2 id="准备工作">准备工作</h2>
<p>在开始之前，你需要：</p>
<h3 id="获取-azure-订阅">获取 Azure 订阅</h3>
<p>拥有一个 <strong>Azure 订阅</strong>，如果您不清楚如何获取 Azure 订阅，可以参考<a href="/zh/blog/2024/build-own-chatgpt-on-azure/#%E5%85%88%E5%86%B3%E6%9D%A1%E4%BB%B6">之前文章中的注册 Azure 订阅内容</a>进行操作。</p>
<h3 id="创建-azure-ai-foundry">创建 Azure AI Foundry</h3>
<p>首先进入您的 Azure 订阅中的 AI Foundry，展开左侧 All Resources，找到 Azure AI Foundry，点击 Create 创建一个 Azure AI Foundry：</p>
<p><img alt="创建 Azure AI Foundry" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/1-azure-ai-foundry.png"></p>
<p>创建时注意区域选择，由于 Sora 2 模型并未在所有 Azure 区域开放预览，这里建议选择 East US 2 区域：</p>
<p><img alt="选择 AI Foundry 区域" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/2-create-ai-foundry.png"></p>
<h3 id="1-创建-azure-ai-foundry-project">1. 创建 Azure AI Foundry Project</h3>
<p>创建完成后进入您的 Azure AI Foundry 在 All Resources 中找到 Projects，点击 New 创建一个新的 Project：</p>
<p><img alt="创建 Project" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/3-azure-ai-foundry-project.png"></p>
<h3 id="2-打开-azure-ai-foundry-protal">2. 打开 Azure AI Foundry Protal</h3>
<p>创建完成 Project 后，打开您的 Project，点击 Go to Azure AI Foundry protal：</p>
<p><img alt="打开 AI Foundry Portal" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/4-go-to-portal.png"></p>
<p>正常情况下 Azure AI Foundry Protal 如下：</p>
<p><img alt="AI Foundry Portal" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/5-ai-foundry-portal.png"></p>
<h3 id="3-启用和部署-sora-2-模型">3. 启用和部署 Sora 2 模型</h3>
<p>又有 Sora 2 模型在本文发布时仍然处于预览状态，所以我们首先需要申请权限，后续开放使用后可调过本步骤，首先点击左侧的 Model catalog，可以看到 Sora 2 模型的标签，点击 Check out model：</p>
<p><img alt="Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/6-check-out-sora-2.png"></p>
<p>也可以在下方的模型列表找到想要的模型进行使用。</p>
<p>如果您没有使用 Sora 2 模型的权限，进入 Sora 2 模型介绍界面后将如下所示：</p>
<p><img alt="获取 Sora 2 模型权限" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/7-request-access-sora-2.png"></p>
<p>请点击 Request access 链接进行权限申请。</p>
<p>如您已经拥有 Sora 2 访问权限，可以点击 Use this model 进行模型部署：</p>
<p><img alt="使用 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/8-use-sora-2.png"></p>
<p>部署过程非常简单，默认保持各类选项即可进行创建部署：</p>
<p><img alt="部署 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/9-deploy-sora-2.png"></p>
<p>部署完成后，切换左侧菜单至 Models + endpoints，查看模型列表：</p>
<p><img alt="查看 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/10-models-sora-2.png"></p>
<p>至此您就拥有了属于自己的 Sora 2 模型，可以开始使用！</p>
<h2 id="通过-azure-ai-foundry-playground-使用-sora-2">通过 Azure AI Foundry Playground 使用 Sora 2</h2>
<p>Azure AI Foundry 提供了强大的可视化界面，你可以无需写代码就生成视频。</p>
<h3 id="1-进入-playground">1. 进入 Playground</h3>
<p>打开 Azure AI Foundry 左侧菜单中的 Playgrounds，选择 Video playground：</p>
<p><img alt="Video Playground" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/11-video-playground.png"></p>
<p>进入 Playground 后选择 Sora 2 模型：</p>
<p><img alt="选择 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/12-choose-sora-2.png"></p>
<h3 id="2-填写提示词-prompt">2. 填写提示词 Prompt</h3>
<p>示例 Prompt：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>A high-energy cyberpunk chase in the rain with neon reflections on wet streets. A futuristic motorcycle speeds through, dynamic motion blur capturing its sleek design and vibrant city lights.
</span></span></code></pre></div><p><img alt="输入提示词" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/13-input-prompt.png"></p>
<h3 id="3-设置选项">3. 设置选项</h3>
<p>可调节选项包括：</p>
<table>
	<thead>
			<tr>
					<th>选项</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Attach an image</strong></td>
					<td>可以附加图片进行相视频的生成</td>
			</tr>
			<tr>
					<td><strong>Aspect ratio</strong></td>
					<td>视频长宽比，默认为 16:9</td>
			</tr>
			<tr>
					<td><strong>Resolution</strong></td>
					<td>分辨率，当前只提供 720p</td>
			</tr>
			<tr>
					<td><strong>Duration</strong></td>
					<td>输出视频时长（如 5s、10s），默认 8s</td>
			</tr>
	</tbody>
</table>
<p><img alt="设置生成参数" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/14-settings-sora-2.png"></p>
<h3 id="4-生成视频">4. 生成视频</h3>
<p>设置好后，点击 Generate 进行视频生成，视频生成成功后，你可以在 Generation history 中预览或下载视频：</p>
<p><img alt="生成视频" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/15-video.png"></p>
<h2 id="通过-python-调用-sora-2">通过 Python 调用 Sora 2</h2>
<p>Azure 为 OpenAI 模型提供统一的接口，可以直接在你的后端调用。</p>
<h3 id="1-安装-sdk">1. 安装 SDK</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install azure-ai-openai
</span></span><span style="display:flex;"><span>pip install python-dotenv
</span></span></code></pre></div><h3 id="2-配置环境变量">2. 配置环境变量</h3>
<p>创建 <code>.env</code> 文件：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>AZURE_OPENAI_ENDPOINT<span style="color:#f92672">=</span>https://&lt;your-resource-name&gt;.openai.azure.com/
</span></span><span style="display:flex;"><span>AZURE_OPENAI_KEY<span style="color:#f92672">=</span>&lt;your-key&gt;
</span></span><span style="display:flex;"><span>AZURE_OPENAI_API_VERSION<span style="color:#f92672">=</span>2024-10-01-preview
</span></span></code></pre></div><h3 id="3-python-调用示例文生视频">3. Python 调用示例：文生视频</h3>
<p>下面是最简可运行示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> dotenv <span style="color:#f92672">import</span> load_dotenv
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> azure.ai.openai <span style="color:#f92672">import</span> OpenAIClient
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> azure.core.credentials <span style="color:#f92672">import</span> AzureKeyCredential
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>load_dotenv()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> OpenAIClient(
</span></span><span style="display:flex;"><span>    endpoint<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#34;AZURE_OPENAI_ENDPOINT&#34;</span>),
</span></span><span style="display:flex;"><span>    credential<span style="color:#f92672">=</span>AzureKeyCredential(os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#34;AZURE_OPENAI_KEY&#34;</span>)),
</span></span><span style="display:flex;"><span>    api_version<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#34;AZURE_OPENAI_API_VERSION&#34;</span>)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">A cinematic shot of a spaceship flying through a nebula filled with colorful cosmic clouds.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>response <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>get_video(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sora-2&#34;</span>,
</span></span><span style="display:flex;"><span>    prompt<span style="color:#f92672">=</span>prompt,
</span></span><span style="display:flex;"><span>    duration<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span>,              <span style="color:#75715e"># seconds</span>
</span></span><span style="display:flex;"><span>    resolution<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1080p&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 保存视频</span>
</span></span><span style="display:flex;"><span>video_bytes <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>video
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> open(<span style="color:#e6db74">&#34;output.mp4&#34;</span>, <span style="color:#e6db74">&#34;wb&#34;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>    f<span style="color:#f92672">.</span>write(video_bytes)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Video generated: output.mp4&#34;</span>)
</span></span></code></pre></div><h3 id="4-使用结构化-payload">4. 使用结构化 payload</h3>
<p>Sora 还支持更多控制，例如镜头、分镜头脚本、环境细节。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;prompt&#34;</span>: <span style="color:#e6db74">&#34;A cute corgi running on a beach during sunset.&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;duration&#34;</span>: <span style="color:#ae81ff">8</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;resolution&#34;</span>: <span style="color:#e6db74">&#34;720p&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;motion&#34;</span>: <span style="color:#e6db74">&#34;medium&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;style&#34;</span>: <span style="color:#e6db74">&#34;realistic&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>response <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>get_video(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sora-2&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">**</span>payload
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> open(<span style="color:#e6db74">&#34;corgi_beach.mp4&#34;</span>, <span style="color:#e6db74">&#34;wb&#34;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>    f<span style="color:#f92672">.</span>write(response<span style="color:#f92672">.</span>video)
</span></span></code></pre></div><h2 id="生成最佳视频的技巧">生成最佳视频的技巧</h2>
<h3 id="1-提示词结构推荐general-prompt-template">1. 提示词结构推荐（General Prompt Template）</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[主体对象] + [场景环境] + [动作] + [镜头效果] + [光影] + [风格]
</span></span></code></pre></div><p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>A high-energy cyberpunk chase in the rain with neon reflections on wet streets. A futuristic motorcycle speeds through, dynamic motion blur capturing its sleek design and vibrant city lights.
</span></span></code></pre></div><h3 id="2-控制一致性">2. 控制一致性</h3>
<ul>
<li>使用 <strong>seed</strong> 可重复生成风格一致的视频</li>
<li>合理设置 motion level 获得更自然的动作</li>
<li>提供清晰场景描述可减少模型误解</li>
</ul>
<h2 id="常见问题faq">常见问题（FAQ）</h2>
<h3 id="1-我试了但看不到-sora-2">1. 我试了但看不到 Sora 2？</h3>
<p>目前需要访问权限。你需要：</p>
<ul>
<li>联系 Azure 管理员</li>
<li>到模型页面确认已启用</li>
</ul>
<h3 id="2-视频最大时长是多少">2. 视频最大时长是多少？</h3>
<p>具体限制依区域会有不同，通常为 <strong>≤ 10 秒</strong>。</p>
<h3 id="3-python-生成的视频过小或无内容">3. Python 生成的视频过小或无内容？</h3>
<p>确保保存时使用 <code>response.video</code> 而不是错误字段。</p>
<h2 id="总结">总结</h2>
<p>本文介绍了在 <strong>Azure AI Foundry</strong> 中使用 <strong>Sora 2 文生视频模型</strong>的两种方式：</p>
<ul>
<li>使用可视化 <strong>Playground</strong> 快速体验</li>
<li>使用 <strong>Python SDK</strong> 在生产环境自动生成视频</li>
</ul>
<p>Azure 的企业级安全合规能力让 Sora 2 成为企业落地视频生成的重要选择。</p>
]]></content:encoded></item><item><title>玩转大语言模型：基于 vLLM 框架的大模型推理优化实现参数 / 数据（P/D）分离</title><link>https://wilsonwu.me/zh/blog/2025/vllm-pd-separation/</link><pubDate>Mon, 29 Sep 2025 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2025/vllm-pd-separation/</guid><description>随着大语言模型规模的不断增长，推理部署面临显存紧张与并发受限的挑战。本文介绍如何借助 vLLM 框架，通过 参数（Parameter）/ 数据（Data）分离（P/D Separation）优化推理性能。我们将从原理、架构思路到配置与代码实例逐步展开，展示如何让参数长期驻留在专用 GPU，而输入序列与 KV Cache 等动态数据独立管理，从而提升吞吐率、降低成本并增强系统扩展性。</description><content:encoded><![CDATA[<p>大模型在推理部署时，往往存在<strong>显存瓶颈</strong>：</p>
<ul>
<li>模型参数（Parameters, P）动辄上百 GB，需要长期驻留显存。</li>
<li>输入/输出数据（Data, D）则随请求动态变化，但往往和参数耦合在同一设备上，导致显存占用不均衡，扩展性受限。</li>
</ul>
<p>为了解决这一问题，可以借助 <strong>vLLM 框架</strong>实现<strong>参数 / 数据（P/D）分离</strong>，提升推理系统的灵活性和吞吐。</p>
<h2 id="大模型推理的资源瓶颈">大模型推理的资源瓶颈</h2>
<p>以一个 70B 规模的 Transformer 模型为例：</p>
<ul>
<li>参数权重（FP16 存储）约需 <strong>140GB 显存</strong>；</li>
<li>每次请求输入的序列数据、KV Cache 会消耗额外显存，并随 batch size 增长而急剧膨胀。</li>
</ul>
<p>如果不加区分地将 P 与 D 放在同一块 GPU：</p>
<ol>
<li>参数长期驻留，挤压了用于动态数据的显存；</li>
<li>多实例并发时，数据显存不足，限制了吞吐。</li>
</ol>
<p>因此，在分布式推理系统中，业界逐渐采用 <strong>参数与数据分离（P/D Separation）</strong> 的架构思路。</p>
<h2 id="vllm-简介">vLLM 简介</h2>
<p><a href="https://vllm.ai/">vLLM</a> 是一个高性能的大模型推理引擎，核心优势包括：</p>
<ul>
<li><strong>PagedAttention</strong>：高效管理 KV Cache，支持大批量并发；</li>
<li><strong>高吞吐率</strong>：相较于 Hugging Face Transformers 推理，吞吐提升数倍；</li>
<li><strong>灵活的分布式支持</strong>：可结合 DeepSpeed、Megatron 等方案，支持参数/数据分布式存储与调度。</li>
</ul>
<p>vLLM 的模块化架构，使其天然适合实现 <strong>P/D 分离</strong>。</p>
<h2 id="pd-分离的实现思路">P/D 分离的实现思路</h2>
<p>在 vLLM 中，推理流程大致分为两个部分：</p>
<ol>
<li>
<p><strong>参数侧（P）</strong></p>
<ul>
<li>模型权重加载与存放；</li>
<li>可通过 <strong>ZeRO-3 / Tensor Parallel</strong> 等策略将参数分布在多 GPU 节点上；</li>
<li>参数在整个推理生命周期中保持常驻，不随请求波动。</li>
</ul>
</li>
<li>
<p><strong>数据侧（D）</strong></p>
<ul>
<li>动态输入序列、KV Cache、采样状态等；</li>
<li>可使用 <strong>PagedAttention</strong> 管理显存分页；</li>
<li>支持把部分 KV Cache 放到 CPU/Host 内存，再动态迁移到 GPU。</li>
</ul>
</li>
</ol>
<h2 id="核心机制">核心机制</h2>
<p>其核心机制是显存资源隔离：</p>
<ul>
<li><strong>权重（P）</strong> 固定驻留在专门的 GPU（参数节点）；</li>
<li><strong>数据（D）</strong> 可以按需分配到其他 GPU（数据节点），甚至利用 CPU 内存 / NVMe。</li>
</ul>
<p>这样：</p>
<ul>
<li>参数显存不与动态数据抢占空间；</li>
<li>数据可以根据请求动态扩展，提升并发能力。</li>
</ul>
<h2 id="在-vllm-中的实现路径">在 vLLM 中的实现路径</h2>
<p>实现 P/D 分离的常见方法：</p>
<ol>
<li>
<p><strong>参数分片（Parameter Sharding）</strong></p>
<ul>
<li>借助 vLLM 对 Megatron-LM / DeepSpeed 的支持，将模型参数按张量维度切分到多 GPU；</li>
<li>仅在推理计算时聚合，不需要在每个设备上完整存放参数。</li>
</ul>
</li>
<li>
<p><strong>KV Cache 分离（Data Sharding）</strong></p>
<ul>
<li>通过 vLLM 的 <strong>PagedAttention</strong>，可以把 KV Cache 存在独立的 GPU 或 Host 内存；</li>
<li>在推理过程中按需调页，类似操作系统的虚拟内存机制。</li>
</ul>
</li>
<li>
<p><strong>Pipeline 并行化</strong></p>
<ul>
<li>将模型计算划分为不同阶段，前几层专门占用参数节点，后几层在数据节点完成；</li>
<li>在流水线中，数据逐层传递，而参数保持分片驻留。</li>
</ul>
</li>
</ol>
<h2 id="基于-vllm-的-pd-分离实战">基于 vLLM 的 P/D 分离实战</h2>
<p>下面给出两部分内容：</p>
<ul>
<li><strong>配置示例</strong>：如何在多 GPU 环境下指定参数和数据分布；</li>
<li><strong>代码示例</strong>：如何加载模型并运行推理，展示参数常驻、数据动态分配。</li>
</ul>
<h2 id="配置示例">配置示例</h2>
<p>假设我们有 4 张 GPU：</p>
<ul>
<li><strong>GPU 0,1</strong>：专门存放模型参数（P）；</li>
<li><strong>GPU 2,3</strong>：主要承载 KV Cache 等动态数据（D）。</li>
</ul>
<p>我们可以在启动 vLLM 时设置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>CUDA_VISIBLE_DEVICES<span style="color:#f92672">=</span>0,1,2,3 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>python -m vllm.entrypoints.api_server <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --model /path/to/llama-2-70b <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --tensor-parallel-size <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --gpu-memory-utilization 0.85 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --enable-chunked-prefill <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --enforce-eager <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --kv-cache-dtype fp16 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --max-num-batched-tokens <span style="color:#ae81ff">4096</span>
</span></span></code></pre></div><p>这里的关键点：</p>
<ul>
<li><code>--tensor-parallel-size 2</code>：只在 GPU 0,1 上做参数分片存放；</li>
<li><code>--gpu-memory-utilization</code>：控制参数占用的比例，留出余量给数据；</li>
<li><code>--kv-cache-dtype fp16</code>：降低 KV Cache 精度，减少数据显存占用；</li>
<li><strong>vLLM 内部的 PagedAttention</strong> 会自动把 KV Cache 分配到 GPU 2,3，如果溢出则回退到 CPU 内存。</li>
</ul>
<h2 id="代码示例">代码示例</h2>
<p>下面是一个 Python 调用示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> vllm <span style="color:#f92672">import</span> LLM, SamplingParams
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># -------- 参数（P）分离 --------</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># tensor_parallel_size=2 表示参数分布在前两张 GPU（0,1）</span>
</span></span><span style="display:flex;"><span>llm <span style="color:#f92672">=</span> LLM(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/path/to/llama-2-70b&#34;</span>,
</span></span><span style="display:flex;"><span>    tensor_parallel_size<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span>    gpu_memory_utilization<span style="color:#f92672">=</span><span style="color:#ae81ff">0.85</span>,
</span></span><span style="display:flex;"><span>    enforce_eager<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,       <span style="color:#75715e"># 避免显存碎片</span>
</span></span><span style="display:flex;"><span>    enable_chunked_prefill<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    kv_cache_dtype<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;fp16&#34;</span>     <span style="color:#75715e"># 数据（D）使用更轻量的缓存精度</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># -------- 数据（D）分离 --------</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># PagedAttention 会自动管理 KV Cache 的存放位置（GPU2, 3, 甚至 CPU 内存）</span>
</span></span><span style="display:flex;"><span>sampling_params <span style="color:#f92672">=</span> SamplingParams(
</span></span><span style="display:flex;"><span>    temperature<span style="color:#f92672">=</span><span style="color:#ae81ff">0.8</span>,
</span></span><span style="display:flex;"><span>    top_p<span style="color:#f92672">=</span><span style="color:#ae81ff">0.95</span>,
</span></span><span style="display:flex;"><span>    max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">256</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;解释一下通过 vLLM 实现 P/D 分离的好处。&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 运行推理</span>
</span></span><span style="display:flex;"><span>outputs <span style="color:#f92672">=</span> llm<span style="color:#f92672">.</span>generate([prompt], sampling_params)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;模型输出：&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> output <span style="color:#f92672">in</span> outputs:
</span></span><span style="display:flex;"><span>    print(output<span style="color:#f92672">.</span>outputs[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>text)
</span></span></code></pre></div><h2 id="验证方式">验证方式</h2>
<p>运行时可以用 <code>nvidia-smi</code> 观察：</p>
<ul>
<li>GPU 0,1 显存几乎恒定，主要承载参数；</li>
<li>GPU 2,3 的显存随请求数量/序列长度波动，主要承载 KV Cache；</li>
<li>如果输入过大，KV Cache 会溢出到 CPU 内存，但仍可继续推理。</li>
</ul>
<h2 id="实际效果与收益">实际效果与收益</h2>
<p>通过 P/D 分离，可以得到以下好处：</p>
<ul>
<li><strong>提升 GPU 利用率</strong>：参数和数据分工明确，避免显存冲突；</li>
<li><strong>支持更大 batch size</strong>：数据显存可扩展，吞吐率提升显著；</li>
<li><strong>降低成本</strong>：参数可驻留在高端 GPU，而数据可放到中低端 GPU 或 CPU 内存，形成分层存储；</li>
<li><strong>灵活性</strong>：适配多种硬件资源组合（A100 + H100, GPU + CPU, GPU + NVMe）。</li>
</ul>
<p>在实践中，结合 vLLM 的 PagedAttention，P/D 分离的效果尤为明显，可让大模型推理在同样硬件下实现 <strong>数倍并发提升</strong>。</p>
<h2 id="总结">总结</h2>
<p>vLLM 框架凭借其高效的 KV Cache 管理和分布式能力，为 <strong>大模型推理的 P/D 分离</strong> 提供了坚实基础。
通过将 <strong>参数（P）</strong> 和 <strong>数据（D）</strong> 解耦，可以更好地利用硬件资源，提升推理性能与扩展性。</p>
<p>未来，随着 <strong>混合精度存储、分层缓存（GPU+CPU+NVMe）</strong> 的发展，P/D 分离架构将逐渐成为大模型推理的主流范式。</p>
]]></content:encoded></item><item><title>玩转大语言模型：微软最新开源长语音大模型 VibeVoice 入门</title><link>https://wilsonwu.me/zh/blog/2025/microsoft-vibevoice/</link><pubDate>Thu, 18 Sep 2025 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2025/microsoft-vibevoice/</guid><description>本文介绍使用微软最新开源的 VibeVoice 大模型，在本地进行推理实现长文本，多角色文字转语音。</description><content:encoded><![CDATA[<h2 id="什么是-vibevoice">什么是 VibeVoice？</h2>
<p><strong><a href="https://microsoft.github.io/VibeVoice/">VibeVoice</a></strong> 是 Microsoft Research 发布的一套面向<strong>长篇、多说话人、对话式</strong>语音合成的研究框架，目标场景例如整集播客、有声剧或访谈：能在单次生成中维持说话人一致性并处理自然的换手（turn-taking）。模型家族包含多个规模（例如 1.5B、7B 等），并在 Hugging Face 上以 <a href="https://huggingface.co/microsoft/VibeVoice-1.5B"><code>microsoft/VibeVoice-1.5B</code></a> 形式开放（模型卡、模型文件、model card 中有安装/使用与责任使用说明）。</p>
<p>它解决了传统 TTS（Text-To-Speech）系统的一些痛点，比如：</p>
<ul>
<li>难以维持<strong>长时间</strong>对话的语音一致性（speaker consistency）；</li>
<li>多说话人的切换自然性（turn-taking）差；</li>
<li>效率低 — 长文本 + 多说话人时，资源消耗大。</li>
</ul>
<h2 id="核心创新与架构">核心创新与架构</h2>
<p>VibeVoice 有几个比较新的或者关键的技术设计：</p>
<table>
	<thead>
			<tr>
					<th>组件</th>
					<th>功能 / 目的</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Continuous Speech Tokenizers</strong>（声学 + 语义两种）</td>
					<td>用来把音频压缩成低帧率（7.5 Hz）表示，同时保留语义与音质信息。声学 token 与语义 token 分别负责声音细节和内容表达。</td>
			</tr>
			<tr>
					<td><strong>LLM 基础模型</strong>（Large Language Model）</td>
					<td>在该版本里用的是 Qwen2.5-1.5B，用来处理文本、说话人信息以及上下文对话流。</td>
			</tr>
			<tr>
					<td><strong>Diffusion Head</strong></td>
					<td>对声学 VAE 的特征进行预测，是生成高保真声音细节的模块。这个模块较轻 (大致 4 层结构)，在推理阶段使用 diffusion 的技术（包括去噪等）。</td>
			</tr>
			<tr>
					<td><strong>上下文长度 &amp; 多说话人</strong></td>
					<td>支持高达 <strong>90 分钟</strong>语音生成，最多 <strong>4 个</strong>说话人。</td>
			</tr>
	</tbody>
</table>
<p>架构图如下：</p>
<p><img alt="VibeVoice 架构" loading="lazy" src="/zh/blog/2025/microsoft-vibevoice/vibevoice-arch.jpg"></p>
<h2 id="优点和局限">优点和局限</h2>
<h3 id="优点">优点</h3>
<ol>
<li><strong>长篇幅对话能力</strong> — 能生成近 90 分钟的连续对话，并维持说话人一致性。</li>
<li><strong>多说话人支持</strong> — 最多支持 4 个不同说话人的切换，且对话流程自然。</li>
<li><strong>压缩效率高</strong> — 用 7.5 Hz 帧率的 speech tokenizer，音频被高度压缩但仍保证质量。</li>
<li><strong>语义 + 声学分离</strong> — 语义 tokenizer（semantic）负责内容，声学 tokenizer（acoustic）负责声音细节，两者协同。</li>
</ol>
<h3 id="局限">局限</h3>
<ol>
<li><strong>语言支持有限</strong> — 目前主要是英语与中文，跨语言能力还在研究中。</li>
<li><strong>背景音乐 / 音效不支持</strong> — 当前专注纯语音合成，没有加入背景音或重叠说话人（speaker overlap）。</li>
<li><strong><a href="https://github.com/microsoft/VibeVoice">仓库暂时关闭</a></strong> — 官方声明因 “部分用途超出初衷 / 不符合责任使用” 的情况，仓库暂时被 disable。意味着可能无法立即拿到源码或全部功能。</li>
<li><strong>硬件要求 / 资源消耗</strong> — 虽然压缩效率高，但生成长篇语音 + 多说话人 + 高保真度仍需要较好的 GPU 资源。</li>
<li><strong>潜在滥用风险</strong> — 用来做语音克隆、假扮、deepfake 等可能的恶意用途。官方在文档中也提到要谨慎。</li>
</ol>
<h2 id="快速上手">快速上手</h2>
<p>下面给出具体实现步骤及上手流程。注意：由于官方仓库目前不包含完整脚本，实际推理通常依赖社区实现或 Hugging Face Spaces / Colab 示例。所列步骤已参考 Hugging Face 模型页与社区示例仓库。</p>
<h3 id="先决条件">先决条件</h3>
<ul>
<li>
<p>Python 3.8+；建议在隔离虚拟环境（venv / conda）中操作。</p>
</li>
<li>
<p>GPU：带 CUDA 的 NVIDIA GPU（具体 VRAM 与性能见第 5 节）。若使用 Apple Silicon / macOS，可参见社区讨论与专门脚本。</p>
</li>
<li>
<p>安装工具（示例）：</p>
<ul>
<li><code>pip install huggingface_hub transformers accelerate torch soundfile torchaudio numpy einops</code></li>
<li>本地需要 <code>ffmpeg</code>（用于音频切分/拼接与格式转换）。</li>
</ul>

<blockquote>
  <p>具体版本请按 PyTorch 官网或社区脚本说明选择（不同 CUDA 版本的 wheel 不同）。参考社区仓库的 requirements 列表以确保兼容。</p>

</blockquote>
</li>
</ul>
<h3 id="下载模型权重">下载模型权重</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> huggingface_hub <span style="color:#f92672">import</span> snapshot_download
</span></span><span style="display:flex;"><span>model_dir <span style="color:#f92672">=</span> snapshot_download(<span style="color:#e6db74">&#34;microsoft/VibeVoice-1.5B&#34;</span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;模型已下载到&#34;</span>, model_dir)
</span></span></code></pre></div><p>注意：<code>snapshot_download</code> 会把 HF 模型文件和模型卡同步拉取到本地；若模型文件较大，请确保磁盘与网络条件。</p>
<h3 id="使用社区推理脚本">使用社区推理脚本</h3>
<p>很多社区仓库或 Hugging Face Spaces 提供了 <code>demo/inference_from_file.py</code> 之类的脚本。示例命令：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 在 Colab / 本地环境中操作</span>
</span></span><span style="display:flex;"><span>git clone https://huggingface.co/technicalheist/vibevoice-1.5b
</span></span><span style="display:flex;"><span>cd vibevoice-1.5b
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 安装 Python 依赖（按模型页 “Requirements” 部分）</span>
</span></span><span style="display:flex;"><span>pip install torch transformers soundfile ffmpeg-python  <span style="color:#75715e"># plus 其他必要包</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 创建一个 transcript 文件，比如 transcript.txt</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Speaker 1: Hello, how are you doing?
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Speaker 2: I&#39;m good, thanks. What about you?&#34;</span> &gt; transcript.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 用 demo 脚本生成语音</span>
</span></span><span style="display:flex;"><span>python demo/inference_from_file.py <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --model_path /path/to/technicalheist/vibevoice-1.5b <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --txt_path transcript.txt <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --speaker_names <span style="color:#e6db74">&#34;Alice,Bob&#34;</span>
</span></span></code></pre></div><p>社区实现（例如 <code>technicalheist/vibevoice-1.5b</code>、<code>shijincai/VibeVoice</code> 等）提供了可运行的示例与依赖说明；使用前请仔细阅读其 README 并依据其中的依赖和指引来配置环境。示例脚本名与参数在不同实现中可能不同，请以具体仓库中的文档为准。</p>
<h3 id="简单示例">简单示例</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>Speaker 1: Hello, welcome to our podcast.
</span></span><span style="display:flex;"><span>Speaker 2: Thanks — happy to be here. Let&#39;s talk about VibeVoice.
</span></span><span style="display:flex;"><span>Speaker 1: Sure, start with the key idea...
</span></span></code></pre></div><p>将上面保存为 <code>transcript.txt</code> 并传入 <code>--txt_path</code> 即可（脚本会解析说话人标记并输出对应音轨）。社区脚本通常会把每个说话人的音色/说话风格作为参数或示例内置。</p>
<h2 id="结论">结论</h2>
<p>微软的 VibeVoice 是当前 TTS / 语音合成领域一个非常令人兴奋的进展。它在<strong>长篇幅语音 + 多说话人</strong>上取得了突破，尤其是在对话流畅性和说话人一致性方面，技术设计（如低帧率的双 tokenizers + next-token diffusion + LLM 架构）也很前沿。</p>
<p>如果你正在做播客、有声书或者对话剧本的语音合成，这可能是一个非常有用的工具／研究基础。不过在商业部署或者包含敏感语音身份／法律边界用途时，需要格外注意责任使用，以及是否有完整的许可与风险评估。</p>
]]></content:encoded></item><item><title>玩转大语言模型：使用 SGLang 框架实现大语言模型推理入门教程</title><link>https://wilsonwu.me/zh/blog/2025/getting-started-with-sglang/</link><pubDate>Thu, 10 Jul 2025 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2025/getting-started-with-sglang/</guid><description>这篇文章带你从零开始，快速上手 SGLang：了解它是什么、为什么快、如何安装与启动、如何用 OpenAI 兼容接口调用，并通过一个完整的 Demo（含流式输出与并发调用）一步步跑通。文章面向刚接触 SGLang 的同学，几分钟内即可看到本地模型在线响应。</description><content:encoded><![CDATA[<p>随着大语言模型热度的升级，企业和个人使用者的研究重心逐步从训练转移至推理（说白了是由造轮子转变为务实的使用）。而在模型推理领域，最炙手可热的两大框架无疑是 vLLM 和 SGLang，而作为后起之秀的 SGLang，其表现也值得大家关注，今天就基于 SGLang 为大家带来一篇入门教程文章，希望能帮助更多希望了解大语言模型及 SGLang 框架的朋友。</p>
<h2 id="sglang-简介">SGLang 简介</h2>
<p>SGLang 是一款面向大语言模型（LLM）和视觉语言模型（VLM）的高性能推理框架，通过精心设计的后端运行时与前端语言协同工作，使模型交互更加高效且可控。其核心优势包括：</p>
<ul>
<li><strong>高效后端运行时</strong>：采用创新的 RadixAttention 技术实现前缀缓存，支持跳跃式受限解码、零开销 CPU 调度、连续批处理、令牌注意力（分页注意力）、张量并行、FlashInfer 内核、分块预填充以及多种量化技术（FP8/INT4/AWQ/GPTQ），显著提升推理效率。</li>
<li><strong>灵活前端语言</strong>：提供直观且强大的 LLM 编程接口，支持链式生成调用、高级提示工程、复杂控制流、多模态输入、并行处理及外部系统交互。</li>
<li><strong>广泛模型兼容性</strong>：支持多种主流生成式模型（Llama、Gemma、Mistral、QWen、DeepSeek、LLaVA 等）、嵌入模型（e5-mistral、gte）及奖励模型（Skywork），并提供简便的新模型扩展机制。</li>
<li><strong>活跃开源生态</strong>：拥有蓬勃发展的社区支持，已获得广泛业界认可（截至 2025 年 3 月 17 日，GitHub 累计星标超过 12,000）。</li>
</ul>
<p>其技术架构如下：</p>
<p><img alt="SGLang 架构" loading="lazy" src="/zh/blog/2025/getting-started-with-sglang/1-sglang-architecture.png"></p>
<p>除此之外，对于初学者，需要了解其以下特性：</p>
<ul>
<li><strong>OpenAI 兼容 API</strong>：直接用 openai Python SDK 或 cURL 调用，不用改你的上层业务代码。</li>
<li><strong>高吞吐与低延迟</strong>：结合连续批处理与前缀缓存等技巧，让相同前缀的请求复用计算。</li>
<li><strong>生产友好</strong>：支持多并发、流式输出、可与 Hugging Face 模型库直接对接。</li>
</ul>
<h2 id="环境准备">环境准备</h2>
<p>工欲善其事必先利其器，要完成本文的新手实验，需准备如下环境：</p>
<ul>
<li>操作系统：建议 Linux（常用为 Ubuntu 20.04+）。WSL2 也可尝试。</li>
<li>Python：建议 3.10 或 3.11。</li>
<li>GPU：建议 NVIDIA 显卡，24GB 显存可跑 7B/8B 级（如 Llama 3.1 8B）。没有 GPU 也能跑小模型或量化模型，但性能有限。</li>
<li>模型来源：Hugging Face（如 meta-llama/Llama-3.1-8B-Instruct）或本地路径。</li>
</ul>
<h2 id="安装-sglang">安装 SGLang</h2>
<p>环境就绪后，使用 pip 安装 SGLang（确保已安装 pip、可选创建虚拟环境）。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>pip install sglang openai huggingface<span style="color:#f92672">-</span>hub
</span></span></code></pre></div><p>如果你需要从 Hugging Face 自动下载私有或受限模型，请先登录或设置 Token：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>huggingface-cli login
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 或</span>
</span></span><span style="display:flex;"><span>export HF_TOKEN<span style="color:#f92672">=</span>您的Token
</span></span></code></pre></div><h2 id="启动-sglang-服务">启动 SGLang 服务</h2>
<p>可以通过以下两种启动方式进行 SGLang 服务的启动。不同版本的 SGLang 命令行参数可能略有差异，如果出现参数名称不匹配，先执行 &ndash;help 查看本机版本支持的参数。</p>
<h3 id="通过-launcher-启动">通过 launcher 启动</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python -m sglang.launcher <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --model meta-llama/Llama-3.1-8B-Instruct <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --host 0.0.0.0 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --port <span style="color:#ae81ff">30000</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --tp <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --max-model-len <span style="color:#ae81ff">8192</span>
</span></span></code></pre></div><p>其中常用参数说明如下：</p>
<ul>
<li>&ndash;model：Hugging Face 模型名称或本地模型权重路径</li>
<li>&ndash;host/&ndash;port：服务监听地址与端口</li>
<li>&ndash;tp：Tensor Parallel 并行度（单卡设为 1；多卡可根据需要设为 2、4 等）</li>
<li>&ndash;max-model-len：上下文最大长度</li>
</ul>
<p>另外需要注意以下两点：</p>
<ul>
<li>如果使用的是 Hugging Face 模型，首次运行会下载模型权重，请确保网络通畅。</li>
<li>如果显存吃紧，可优先尝试更小模型，或减少 &ndash;max-model-len。</li>
</ul>
<h3 id="通过-sglang-命令启动">通过 sglang 命令启动</h3>
<p>如果您使用的版本提供 sglang serve 命令，也可使用如下命令进行启动：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sglang serve -m meta-llama/Llama-3.1-8B-Instruct --host 0.0.0.0 --port <span style="color:#ae81ff">30000</span>
</span></span></code></pre></div><h2 id="快速验证">快速验证</h2>
<p>服务启动后，OpenAI 兼容接口即可使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl http://127.0.0.1:30000/v1/chat/completions <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;Authorization: Bearer EMPTY&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;model&#34;: &#34;meta-llama/Llama-3.1-8B-Instruct&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;messages&#34;: [
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: &#34;You are a helpful assistant.&#34;},
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;用两句话介绍一下你自己。&#34;}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ],
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;max_tokens&#34;: 128,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;temperature&#34;: 0.2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }&#39;</span>
</span></span></code></pre></div><p>如果运行正常，将返回标准的 ChatCompletion JSON 响应。</p>
<h2 id="使用-python-openai-sdk-进行调用">使用 Python OpenAI SDK 进行调用</h2>
<p>非流式调用（最简单的问答）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> openai <span style="color:#f92672">import</span> OpenAI
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> OpenAI(
</span></span><span style="display:flex;"><span>    base_url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://127.0.0.1:30000/v1&#34;</span>,
</span></span><span style="display:flex;"><span>    api_key<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;EMPTY&#34;</span>  <span style="color:#75715e"># 本地服务通常不校验</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>messages <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;system&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;You are a helpful assistant who answers in Chinese.&#34;</span>},
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;请用三句话介绍帕累托最优。&#34;</span>}
</span></span><span style="display:flex;"><span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>resp <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>chat<span style="color:#f92672">.</span>completions<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;meta-llama/Llama-3.1-8B-Instruct&#34;</span>,
</span></span><span style="display:flex;"><span>    messages<span style="color:#f92672">=</span>messages,
</span></span><span style="display:flex;"><span>    temperature<span style="color:#f92672">=</span><span style="color:#ae81ff">0.2</span>,
</span></span><span style="display:flex;"><span>    max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">256</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(resp<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>message<span style="color:#f92672">.</span>content)
</span></span></code></pre></div><p>流式输出（边生成边打印）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> openai <span style="color:#f92672">import</span> OpenAI
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> OpenAI(base_url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://127.0.0.1:30000/v1&#34;</span>, api_key<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;EMPTY&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>messages <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;system&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;You are a helpful assistant that speaks Chinese.&#34;</span>},
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;简述 Transformer 的核心思想，并给出一个现实类比。&#34;</span>}
</span></span><span style="display:flex;"><span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>stream <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>chat<span style="color:#f92672">.</span>completions<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;meta-llama/Llama-3.1-8B-Instruct&#34;</span>,
</span></span><span style="display:flex;"><span>    messages<span style="color:#f92672">=</span>messages,
</span></span><span style="display:flex;"><span>    temperature<span style="color:#f92672">=</span><span style="color:#ae81ff">0.2</span>,
</span></span><span style="display:flex;"><span>    max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">300</span>,
</span></span><span style="display:flex;"><span>    stream<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> chunk <span style="color:#f92672">in</span> stream:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> chunk<span style="color:#f92672">.</span>choices <span style="color:#f92672">and</span> chunk<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>delta:
</span></span><span style="display:flex;"><span>        delta <span style="color:#f92672">=</span> chunk<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>delta<span style="color:#f92672">.</span>content
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> delta:
</span></span><span style="display:flex;"><span>            print(delta, end<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>, flush<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">[Done]&#34;</span>)
</span></span></code></pre></div><p>通过以上两种方式，我们可以构建自己的推理服务应用，也可以通过市面上开源的软件来接入您部署的推理服务，如 Open-WebUI 等。</p>
<h2 id="常见问题与排错">常见问题与排错</h2>
<ul>
<li>启动时报显存不足（CUDA OOM）
<ul>
<li>尝试更小的模型（如 7B/8B）、减小 <code>--max-model-len</code>、或降低并发。</li>
<li>如果有多卡，增加 &ndash;tp 并用多卡分摊显存。</li>
</ul>
</li>
<li>拉取模型失败或速度慢
<ul>
<li>先执行 <code>huggingface-cli login</code>；或提前手动把模型权重下载到本地路径，并在 <code>--model</code> 里填本地路径。</li>
</ul>
</li>
<li>API 报 <code>Unknown argument</code>
<ul>
<li>不同版本的命令行参数可能有差异。请运行：<code>python -m sglang.launcher --help</code></li>
<li>根据你安装的版本调整参数名，如 <code>--model</code> / <code>--model-path</code>、<code>--tp</code> / <code>--tensor-parallel-size</code> 等。</li>
</ul>
</li>
<li>Python 客户端 <code>401/Key</code> 错误
<ul>
<li>本地服务通常不校验 Key，只要带上一个占位即可（如 <code>EMPTY</code>）。</li>
</ul>
</li>
<li>流式输出不打印
<ul>
<li>确认传了 <code>stream=True</code>；迭代 chunk 时取 <code>choices[0].delta.content</code>。</li>
</ul>
</li>
<li>并发性能不达预期
<ul>
<li>尝试用 Async 客户端并发请求；避免在单线程里串行等待。</li>
<li>检查 GPU 利用率；确认没有 CPU/GPU 数据传输瓶颈。</li>
</ul>
</li>
</ul>
<h2 id="总结">总结</h2>
<p>至此，你已经完成了：</p>
<ul>
<li>安装与启动 SGLang 服务</li>
<li>用 OpenAI 兼容接口进行非流式与流式推理</li>
</ul>
<p>祝你上手顺利，玩得开心！如果遇到命令行参数或 API 的小差异，参考本地版本的 &ndash;help 与官方文档进行快速适配即可。</p>
]]></content:encoded></item><item><title>玩转大语言模型：基于 Azure AI Foundry 轻松部署使用 DeepSeek-R1</title><link>https://wilsonwu.me/zh/blog/2025/deploy-deepseek-r1-on-azure/</link><pubDate>Mon, 10 Feb 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/deploy-deepseek-r1-on-azure/</guid><description>&lt;p&gt;DeepSeek 的爆火让人们再一次看到了 AI 的魅力，而随之而来的不仅不是对算力需求的减少，而是在低成本亲民化人工智能中构建更多 AI 业务场景带来的另一波算力需求。今天我们来通过 Azure AI Foundry（原 Azure AI Studio）快速体验 DeepSeek 的风采。&lt;/p&gt;</description><content:encoded><![CDATA[<p>DeepSeek 的爆火让人们再一次看到了 AI 的魅力，而随之而来的不仅不是对算力需求的减少，而是在低成本亲民化人工智能中构建更多 AI 业务场景带来的另一波算力需求。今天我们来通过 Azure AI Foundry（原 Azure AI Studio）快速体验 DeepSeek 的风采。</p>
<h2 id="先决条件">先决条件</h2>
<p>首先需要拥有 Azure 订阅，新用户参考<a href="https://wilsonwu.me/zh/blog/2024/build-own-chatgpt-on-azure/#%E5%85%88%E5%86%B3%E6%9D%A1%E4%BB%B6">玩转大语言模型：无需任何代码通过 Azure OpenAI 服务构建个人专属 ChatGPT</a>中步骤进行开通。然后在 Azure 订阅中创建 AI Foundry 及相关资源，可以参考<a href="https://wilsonwu.me/zh/blog/2024/build-own-chatgpt-on-azure/#%E6%AD%A5%E9%AA%A4-1%E5%88%9B%E5%BB%BA-azure-ai-studio-%E6%9C%8D%E5%8A%A1">创建 Azure AI Foundry 服务</a>中的步骤，这里不再赘述。</p>
<p>一切完成后进入 Azure AI Foundry 首页，并打开名为 main 的 Project，界面如下：</p>
<p><img alt="Azure AI Foundry" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/0-ai-foundry.png"></p>
<h2 id="部署-deepseek-r1-大语言模型">部署 DeepSeek-R1 大语言模型</h2>
<p>点击左侧菜单中的 Model catalog 进入模型列表页：</p>
<p><img alt="模型列表" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/1-models.png"></p>
<p>这里可以看到 Azure 提供了超过 1800 种模型，满足用户全方位的需求。</p>
<p>在搜索框输入 DeepSeek 来查看 Azure 支持的 DeepSeek 模型种类：</p>
<p><img alt="DeepSeek" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/1-deepseek.png"></p>
<p>其中，第一个是全量版 DeepSeek-R1 671B 模型：</p>
<p><img alt="DeepSeek-R1 671B" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/1-deepseek-r1-671b.png"></p>
<p>另一个是经过 NPU 优化的基于 Qwen 的 DeepSeek-R1 1.5B 蒸馏版：</p>
<p><img alt="DeepSeek R1 Qwen 1.5B" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/1-deepseek-r1-qwen-1.5b.png"></p>
<p>我们选择 671B 版本进行部署尝试，进入模型详情后点击 Deploy 进行部署确认：</p>
<p><img alt="部署 DeepSeek-R1" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/1-deploy-deepseek.png"></p>
<p>再点击 Deploy 完成部署，部署结束后会自动跳转至 My assets 中的 Models + endpoints 详情中：</p>
<p><img alt="模型详情" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/1-deployed-model-detail.png"></p>
<p>至此 DeepSeek-R1 671B 部署完成，整个过程非常简单。</p>
<h2 id="调用-deepseek-r1-进行推理">调用 DeepSeek-R1 进行推理</h2>
<p>接下来我们尝试调用一下刚才部署的模型进行对话，进入刚才部署的模型详情中，点击 Consume Tab：</p>
<p><img alt="使用 DeepSeek" loading="lazy" src="/zh/blog/2025/deploy-deepseek-r1-on-azure/2-model-consume.png"></p>
<p>这里我们选择 Python 代码来进行尝试，代码如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># pip install azure-ai-inference</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> azure.ai.inference <span style="color:#f92672">import</span> ChatCompletionsClient
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> azure.core.credentials <span style="color:#f92672">import</span> AzureKeyCredential
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>api_key <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#34;AZURE_INFERENCE_CREDENTIAL&#34;</span>, <span style="color:#e6db74">&#39;这里填写该部署在详情页面看到的 Key 值&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> api_key:
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">Exception</span>(<span style="color:#e6db74">&#34;A key should be provided to invoke the endpoint&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> ChatCompletionsClient(
</span></span><span style="display:flex;"><span>    endpoint<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;https://DeepSeek-R1-gxmog.eastus.models.ai.azure.com&#39;</span>,
</span></span><span style="display:flex;"><span>    credential<span style="color:#f92672">=</span>AzureKeyCredential(api_key)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>model_info <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>get_model_info()
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Model name:&#34;</span>, model_info<span style="color:#f92672">.</span>model_name)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Model type:&#34;</span>, model_info<span style="color:#f92672">.</span>model_type)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Model provider name:&#34;</span>, model_info<span style="color:#f92672">.</span>model_provider_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;messages&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;我即将前往北京，我能在那里看到什么景色？&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;assistant&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;北京，中国的首都&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;关于 #1 有哪些令人惊叹的内容？&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;max_tokens&#34;</span>: <span style="color:#ae81ff">2048</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>response <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>complete(payload)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Response:&#34;</span>, response<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>message<span style="color:#f92672">.</span>content)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Model:&#34;</span>, response<span style="color:#f92672">.</span>model)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Usage:&#34;</span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;	Prompt tokens:&#34;</span>, response<span style="color:#f92672">.</span>usage<span style="color:#f92672">.</span>prompt_tokens)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;	Total tokens:&#34;</span>, response<span style="color:#f92672">.</span>usage<span style="color:#f92672">.</span>total_tokens)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;	Completion tokens:&#34;</span>, response<span style="color:#f92672">.</span>usage<span style="color:#f92672">.</span>completion_tokens)
</span></span></code></pre></div><p>创建一个 .py 文件（如：ds-demo.py），将上面的代码粘贴到其中，替换部署自身的 Key 到 api_key 变量中，然后执行如下命令安装依赖包：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install azure-ai-inference
</span></span></code></pre></div><p>然后执行代码：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python ds-demo.py
</span></span></code></pre></div><p>正常情况下输出结果类似如下内容：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Model name: deepseek-r1
</span></span><span style="display:flex;"><span>Model type: chat-completion
</span></span><span style="display:flex;"><span>Model provider name: DeepSeek
</span></span><span style="display:flex;"><span>Response: &lt;think&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&lt;/think&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>北京作为中国的首都，拥有悠久的历史和丰富的文化遗产，同时也是现代化大都市的典范。以下是一些您在北京可以体验到的精彩内容：
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>1. **历史文化景点**：
</span></span><span style="display:flex;"><span>   - **故宫（紫禁城）**：明清两代的皇家宫殿，世界文化遗产，展示了中国古代建筑和宫廷文化的精华。
</span></span><span style="display:flex;"><span>   - **天坛**：明清皇帝祭天的场所，以其独特的建筑风格和象征意义闻名。
</span></span><span style="display:flex;"><span>   - **长城（如八达岭、慕田峪段）**：世界奇迹之一，是中华民族的象征，登临可感受壮丽的山川与历史厚重感。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>2. **现代都市风貌**：
</span></span><span style="display:flex;"><span>   - **北京CBD**：国贸大厦、中央电视台总部大楼（“大裤衩”）等现代建筑群，体现了北京的经济发展活力。
</span></span><span style="display:flex;"><span>   - **奥林匹克公园**：2008年奥运会主会场，鸟巢、水立方等标志性建筑夜晚灯光璀璨。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>3. **文化体验**：
</span></span><span style="display:flex;"><span>   - **胡同与四合院**：南锣鼓巷、什刹海周边保留了老北京的传统居住形态，可体验胡同骑行或品尝京味小吃。
</span></span><span style="display:flex;"><span>   - **京剧与杂技表演**：老舍茶馆、梨园剧场等地可欣赏中国传统艺术。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>4. **自然景观**：
</span></span><span style="display:flex;"><span>   - **颐和园**：皇家园林典范，融合江南园林设计与北方山水，适合泛舟昆明湖或漫步长廊。
</span></span><span style="display:flex;"><span>   - **香山公园**：秋季红叶漫山，是赏秋热门目的地。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>5. **美食推荐**：
</span></span><span style="display:flex;"><span>   - **北京烤鸭**：全聚德、大董等名店提供地道的烤鸭体验。
</span></span><span style="display:flex;"><span>   - **涮羊肉**：东来顺等老字号以铜锅涮肉为特色。
</span></span><span style="display:flex;"><span>   - **小吃街**：牛街、簋街汇聚各类京味及全国风味小吃。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>6. **其他亮点**：
</span></span><span style="display:flex;"><span>   - **798艺术区**：旧工厂改造的当代艺术聚集地，适合摄影和艺术爱好者。
</span></span><span style="display:flex;"><span>   - **国家博物馆**：馆藏丰富，可系统了解中华文明五千年历史。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>北京融合了古老与现代，传统与创新，无论是对历史感兴趣，还是想感受都市繁华，都能在这里找到独特的体验。建议根据季节选择合适的游览时间（如秋季气候宜人），并提前规划行程以避开高峰人流。
</span></span><span style="display:flex;"><span>Model: deepseek-r1
</span></span><span style="display:flex;"><span>Usage:
</span></span><span style="display:flex;"><span>        Prompt tokens: 31
</span></span><span style="display:flex;"><span>        Total tokens: 522
</span></span><span style="display:flex;"><span>        Completion tokens: 491
</span></span></code></pre></div><h2 id="总结">总结</h2>
<p>DeepSeek 的优势在于其强大的自然语言处理能力和高效的推理性能。通过 Azure AI Foundry 部署，用户可以快速上手并体验其强大的功能。DeepSeek-R1 提供了多种模型版本，满足不同需求，从全量版到经过优化的蒸馏版，用户可以根据实际应用场景选择合适的模型。</p>
]]></content:encoded></item><item><title>玩转大语言模型：无需任何代码通过 Azure OpenAI 服务构建个人专属 ChatGPT</title><link>https://wilsonwu.me/zh/blog/2024/build-own-chatgpt-on-azure/</link><pubDate>Tue, 25 Jun 2024 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2024/build-own-chatgpt-on-azure/</guid><description>通过简单的界面操作，无需编写任何代码即可通过 Azure OpenAI 服务构建自己专属的 ChatGPT 应用。</description><content:encoded><![CDATA[<p>日常使用 ChatGPT 来帮助我们解决工作和生活中的问题已经变成习惯，但偶尔重度使用官方 GPT-4o 后发现会出现临时限额问题，今天我们就来手把手教大家如何轻松通过 Azure OpenAI 服务构建一个专属 ChatGPT 应用。</p>
<h2 id="先决条件">先决条件</h2>
<p>在开始之前，请确保您拥有 Azure 国际版订阅服务，如果您还没有 Azure 订阅，可以通过 <a href="https://azure.microsoft.com/en-us/pricing/purchase-options/pay-as-you-go/?wt.mc_id=MVP_373702">Pay-as-you-go</a> 轻松开启一个 Azure 订阅：</p>
<p><img alt="Pay-as-you-go Azure 订阅" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/pay-as-you-go-azure.png"></p>
<p>注意：开启 Pay-as-you-go Azure 订阅必须绑定国际信用卡，对于新用户还有 200 美金的免费额度，我们完全可以使用这些额度充分使用 Azure 的各项服务。</p>
<p>下面介绍的内容除了与 GPT-4o 交互产生的费用外，其他都是免费的，日常不适用的话不会产生任何费用，GPT-4o 的费用也可以完全使用免费额度覆盖，所以大家不用担心信用卡被刷爆。</p>
<p>成功拥有 Azure 订阅后，然我们开始吧！</p>
<p><img alt="Azure 订阅首页" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/azure-home.png"></p>
<h2 id="步骤-1创建-azure-ai-studio-服务">步骤 1：创建 Azure AI Studio 服务</h2>
<p>在 Azure 订阅主页上方搜索栏中输入 azure ai，找到 Azure AI Studio：</p>
<p><img alt="Azure AI Studio" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/1-start.png"></p>
<p>点击进入 Azure AI Studio 资源页面：</p>
<p><img alt="创建 Azure AI Studio" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/1-create-ai.png"></p>
<p>点击 + New Azure AI Hub 进入创建页面：</p>
<p><img alt="创建 Azure AI Hub" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/1-ai-hub.png"></p>
<p>从界面可以看到创建 Azure AI Hub 共分为 7 个步骤，这里按照指引一路 Next 都选择默认即可，其中需要注意的是区域建议选择 East US，由于免费订阅有一些资源使用限制，目前根据经验来看 East US 区域的使用便利性更佳。</p>
<p>另外是费用问题，可以参考 <a href="https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/?wt.mc_id=MVP_373702">Azure OpenAI 费用详情</a>页面来查看各个模型的收费情况：</p>
<p><img alt="价格" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/1-price.png"></p>
<p>创建完成后返回 Azure AI Studio 资源页面：</p>
<p><img alt="Azure AI Studio" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/1-created.png"></p>
<p>可以看到我们创建了一个 Azure AI hub 和一个配套的 Azure AI services 资源。</p>
<h2 id="步骤-2开始使用-azure-ai-studio">步骤 2：开始使用 Azure AI Studio</h2>
<p>点击打开我们在第一步中创建的 Azure AI Hub 资源，我们会看到详情中醒目的 Launch Azure AI Studio 按钮：</p>
<p><img alt="Azure AI Studio 资源" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/2-use-ai.png"></p>
<p>点击后进入一个全新的 Azure AI Studio 界面：</p>
<p><img alt="Azure AI Studio 界面" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/2-ai-studio.png"></p>
<p>这里将是我们主要工作的区域，点击列表中我们刚创建好的 AI Hub，当然在这里我们依然可以创建新的 AI Hub：</p>
<p><img alt="打开 AI Hub" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/2-open-hub.png"></p>
<p>在 AI Hub 中，我们可以看到其相关联的所有资源：</p>
<p><img alt="AI Hub 详情" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/2-hub-detail.png"></p>
<p>首先我们需要创建一个 Project，点击左侧菜单中的 All projects，然后选择列表上方的 + New Project 按钮：</p>
<p><img alt="新建 Project" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/2-new-project.png"></p>
<p>创建 Project 非常简单，只需要输入名称即可：</p>
<p><img alt="项目名称" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/2-project.png"></p>
<p>创建完成后将自动进入 Project 详情页面：</p>
<p><img alt="项目详情" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/2-project-detail.png"></p>
<p>现在可以熟悉一下 AI Studio，Hub，Project 的界面，接下来我们来部署大语言模型！</p>
<h2 id="步骤-3部署-gpt-4o-大语言模型">步骤 3：部署 GPT-4o 大语言模型</h2>
<p>依然在 Project 界面，在左侧菜单栏下放找到 Deployments 菜单，点击进入后选择 + Create Deployment 按钮：</p>
<p><img alt="创建 Deployment" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/3-create-deployment.png"></p>
<p>在创建界面中我们看到 Azure 为我们提供了 1000 多重大语言模型可供使用，当然，它们要求的计算资源也各不相同，有兴趣可以逐个尝试（注意信用卡不要刷爆），这里我们选择 GPT-4o 模型：</p>
<p><img alt="模型" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/3-models.png"></p>
<p>选定后进入部署页面，按需修改相关选项，确认后点击 Deploy 进行部署：</p>
<p><img alt="选择 GPT-4o" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/3-deploy-gpt-4o.png"></p>
<p>部署成功后我们可以看到 GPT-4o 已经出现在我们的部署列表中：</p>
<p><img alt="部署 GPT-4o" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/3-gpt-4o.png"></p>
<h2 id="步骤-4部署专属-chatgpt-web-应用">步骤 4：部署专属 ChatGPT Web 应用</h2>
<p>确认模型部署正常后，我们进入项目左侧菜单中的 Chat，并选择我们刚才创建的 GPT-4o 模型：</p>
<p><img alt="Chat 功能" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/4-chat.png"></p>
<p>在 Chat 界面我们可以选择大语言模型，简单测试问答功能等，确认一些无误后，我们就可以开始创建专属 Web App 了，选择顶部菜单中的 Deploy to a web app：</p>
<p><img alt="部署 Web App" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/4-web-app.png"></p>
<p>在部署配置中输入名称，选择 Web App 想要部署的订阅，区域等信息，如果希望保存 ChatGPT 的聊天历史，可以开启最下方的选项，但会额外创建 CosmosDB 数据库服务，会产生额外费用：</p>
<p><img alt="Web App 配置" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/4-deploy-web.png"></p>
<p>确认部署后等待完成即可，如果部署正确，那么在 Chat 界面中会有一个 Launch 按钮：</p>
<p><img alt="运行" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/4-launch.png"></p>
<p>点击后会打开您的专属 ChatGPT Web 界面：</p>
<p><img alt="专属 ChatGPT" loading="lazy" src="/zh/blog/2024/build-own-chatgpt-on-azure/4-app-page.png"></p>
<p>注意：默认情况下我们部署的 Web App 是基于您订阅中的账户（IAM）来做用户验证的，如果您自己使用可以保持这个默认行为。</p>
<p>如果您有技术能力且希望对这个专属 ChatGPT Web App 做定制开发，可以前往 <a href="https://github.com/microsoft/sample-app-aoai-chatGPT?wt.mc_id=MVP_373702">Git Repo</a> 查看所有实现细节，可以按需修改为适合自己使用的版本。</p>
<h2 id="总结">总结</h2>
<p>通过 Azure AI 相关的服务我们可以实现很多人工智能相关的实用业务场景，让我们对 AI 和 大语言模型的了解不仅只停留在铺天盖地的消息和无尽的想象，也可以亲身接触实践！</p>
]]></content:encoded></item><item><title>Azure 101 系列课程：Microsoft Azure 概览</title><link>https://wilsonwu.me/zh/blog/azure-101/overview/</link><pubDate>Wed, 19 Jun 2024 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/azure-101/overview/</guid><description>Azure 101 系列课程囊括了最全面的 Microsoft Azure 功能及服务介绍，涵盖了平台的各个方面的能力，本文主要阐述 Azure 整体概述，帮助用户熟悉和了解 Azure 整体能力。</description><content:encoded><![CDATA[<p>Azure 是由 Microsoft 提供的云计算平台和服务。它提供了一系列的基础设施即服务（Infrastructure as a Service，IaaS）、平台即服务（Platform as a Service，PaaS）和软件即服务（Software as a Service，SaaS）解决方案，用于构建、部署和管理各种类型的应用程序和服务。</p>
<h2 id="overview">总览</h2>
<p>Azure 提供了广泛的功能和服务，包括虚拟机、存储、数据库、人工智能、机器学习、区块链、物联网、容器、Serverless 计算等。它还提供了全球范围的数据中心网络，使用户能够在全球范围内部署和扩展应用程序。</p>
<p>Azure 拥有数百项自身提供或合作伙伴提供的服务，截止至 2024 年 6 月 19 日，其拥有 19 个大分类，共计 413 项服务：</p>
<p><img alt="All Service" loading="lazy" src="/zh/blog/azure-101/overview/all-services.png"></p>
<p>使用 Azure，您可以轻松地创建和管理虚拟机，存储和管理数据，构建和部署 Web 应用程序，进行大规模数据分析，实现自动化和监控等。Azure 还提供了强大的安全性和合规性功能，以确保您的数据和应用程序的安全性。</p>
<p>无论您是个人开发者、创业公司还是大型企业，Azure 都提供了灵活的定价模型，以满足不同规模和需求的项目。您可以根据实际使用情况付费，避免了高额的固定成本。</p>
<p>以下将总体介绍 19 个大分类的服务概述。</p>
<h3 id="ai-machine-learning">AI + Machine Learning</h3>
<p>Azure 提供了强大的 AI + 机器学习功能，使用户能够构建和部署智能应用程序和解决方案。通过 Azure 的 AI + 机器学习服务，您可以利用先进的机器学习算法和模型来处理和分析大规模数据，从而获得有价值的洞察和预测能力。</p>
<p><img alt="AI and ML" loading="lazy" src="/zh/blog/azure-101/overview/ai-ml.png"></p>
<p>通过这些 AI + 机器学习服务，您可以实现各种应用场景，如智能推荐、欺诈检测、情感分析、语音识别和自动化决策等。无论您是初学者还是专业人士，Azure 的 AI + 机器学习功能都能帮助您轻松构建和部署智能应用程序。</p>
<h3 id="analytics">Analytics</h3>
<p>Azure Analytics 是 Azure 平台下的一个重要分类，提供了一系列强大的数据分析和洞察服务。这些服务可以帮助用户从海量数据中提取有价值的信息，并进行深入的数据分析和可视化展示。</p>
<p><img alt="Analytics" loading="lazy" src="/zh/blog/azure-101/overview/analytics.png"></p>
<p>Azure Analytics 提供了丰富的数据分析和洞察服务，帮助用户从海量数据中提取有价值的信息，并进行深入的数据分析和可视化展示。无论是处理结构化数据还是非结构化数据，无论是批处理还是实时处理，Azure Analytics 都能满足用户的各种数据分析需求。</p>
<h3 id="compute">Compute</h3>
<p>Compute 计算服务是 Azure 平台下的一个庞大分类，提供了各种计算资源和服务，用于构建、部署和管理应用程序和服务。</p>
<p><img alt="Compute" loading="lazy" src="/zh/blog/azure-101/overview/compute.png"></p>
<p>Azure Compute 提供了多种计算选项，包括虚拟机、容器、Serverless 计算和批处理等。您可以根据应用程序的需求选择适合的计算资源，并根据实际使用情况进行弹性扩展和收缩。</p>
<h3 id="containers">Containers</h3>
<p>Containers 容器分类，提供了一系列容器相关的服务和工具，用于构建、部署和管理容器化应用程序。</p>
<p><img alt="Containers" loading="lazy" src="/zh/blog/azure-101/overview/containers.png"></p>
<p>Azure Containers 提供了强大的容器编排和管理功能，使用户能够轻松地创建、部署和扩展容器化应用程序。通过 Azure 的容器服务，您可以使用 Docker 容器技术来打包应用程序及其依赖项，并在云端或本地环境中运行。</p>
<p>Azure Containers 提供了多种容器服务选项，包括 Azure Kubernetes Service (AKS)、Azure Container Instances (ACI) 和 Azure Container Registry (ACR) 等。这些服务提供了灵活的容器部署和管理方式，适用于不同规模和需求的项目。</p>
<p>使用 Azure Containers，您可以实现容器化应用程序的高可用性、弹性扩展和自动化管理。无论是单个容器应用程序还是复杂的微服务架构，Azure Containers 都能提供可靠的容器化解决方案。</p>
<h3 id="databases">Databases</h3>
<p>Databases 数据库服务提供了多种数据库服务和解决方案，用于存储和管理结构化数据。</p>
<p><img alt="Databases" loading="lazy" src="/zh/blog/azure-101/overview/databases.png"></p>
<p>Azure Databases 提供了多种数据库选项，包括关系型数据库、NoSQL 数据库和缓存服务等。您可以根据应用程序的需求选择适合的数据库类型，并根据实际使用情况进行弹性扩展和收缩。</p>
<p>无论您是需要传统的关系型数据库、灵活的 NoSQL 数据库还是高性能的缓存服务，Azure Databases 都能提供适合您的数据库服务和解决方案。
通过 Azure Databases，您可以轻松地创建和管理数据库实例，进行数据的读写和查询，实现高可用性和数据的持久性。Azure Databases 还提供了强大的安全性和合规性功能，以确保您的数据的安全性和隐私保护。</p>
<h3 id="devops">DevOps</h3>
<p>在 DevOps 中提供了一系列强大的工具和服务，用于实现持续交付和持续集成的 DevOps 实践。</p>
<p><img alt="DevOps" loading="lazy" src="/zh/blog/azure-101/overview/devops.png"></p>
<p>DevOps 中提供了全面的 DevOps 解决方案，包括混沌测试，持续集成、持续交付、测试自动化、应用程序监控和故障排除等。</p>
<h3 id="general">General</h3>
<p>Azure General 分类提供了一系列通用的云服务和工具，是使用 Azure 整个平台的最基础功能。</p>
<p><img alt="General" loading="lazy" src="/zh/blog/azure-101/overview/general.png"></p>
<p>通过 Azure General，您可以找到一系列帮助您快速熟悉和使用 Azure 的工具，并且有效的管理您的各类资源以及计划您的支出，如果您对云计算服务不是特别熟悉或刚刚开始从事相关工作，在 General 中获取帮助是您的很好选择。</p>
<h3 id="hybrid-multicloud">Hybrid + Multicloud</h3>
<p>Azure Hybrid + Multicloud 分类提供了一系列强大的混合云和多云解决方案，帮助用户在不同的云环境中构建、部署和管理应用程序和服务。</p>
<p><img alt="Hybrid + Multicloud" loading="lazy" src="/zh/blog/azure-101/overview/hybrid-mc.png"></p>
<p>Azure Hybrid + Multicloud 提供了多种混合云和多云服务选项，包括混合云连接、数据集成、应用程序迁移和多云管理等。这些服务使用户能够在不同的云环境中灵活地部署和管理应用程序，实现资源的共享和协同工作。</p>
<p>通过 Azure Hybrid + Multicloud，您可以实现混合云环境中的数据集成和应用程序迁移，实现跨云平台的协同工作和资源的共享。Azure Hybrid + Multicloud 还提供了强大的多云管理功能，帮助用户统一管理和监控不同云环境中的资源和服务。</p>
<h3 id="identity">Identity</h3>
<p>Azure Identity 分类提供了一系列身份验证和访问管理服务，用于保护应用程序和数据的安全性。</p>
<p><img alt="Identity" loading="lazy" src="/zh/blog/azure-101/overview/identity.png"></p>
<p>Azure Identity 提供了多种身份验证和访问管理服务选项，包括 Azure Active Directory (Azure AD)、Azure AD B2C 和 Azure AD Domain Services 等。这些服务提供了全面的身份验证和访问管理解决方案，帮助用户建立安全的身份验证和访问控制机制。</p>
<p>通过 Azure Identity，您可以实现用户身份验证和访问管理，保护应用程序和数据的机密性和完整性。Azure Identity 还提供了强大的身份验证和访问管理功能，如多因素身份验证、单一登录、角色基于访问控制和身份联合等，帮助用户确保只有授权的用户能够访问应用程序和数据。</p>
<h3 id="integration">Integration</h3>
<p>Azure Integration 提供了一系列强大的集成服务和工具，用于实现应用程序和系统之间的无缝集成和数据交换。</p>
<p><img alt="Integration" loading="lazy" src="/zh/blog/azure-101/overview/integration.png"></p>
<p>Azure Integration 提供了多种集成服务选项，包括消息队列、事件网格、逻辑应用程序和数据工厂等。这些服务使用户能够轻松地构建、部署和管理应用程序和系统之间的集成解决方案。</p>
<p>通过 Azure Integration，您可以实现应用程序和系统之间的实时数据交换和协同工作。Azure Integration 还提供了强大的消息传递和事件处理功能，帮助用户实现高效的数据流和业务流程。</p>
<h3 id="iot">Internet of Things</h3>
<p>Azure Internet of Things (IoT) 分类提供了一系列强大的物联网服务和解决方案，用于连接、监控和管理物理设备和传感器。</p>
<p><img alt="Internet of Things" loading="lazy" src="/zh/blog/azure-101/overview/iot.png"></p>
<p>Azure IoT 提供了多种物联网服务选项，包括设备连接、数据采集、实时分析和远程监控等。这些服务使用户能够轻松地构建、部署和管理物联网解决方案，实现设备之间的互联和数据的实时处理。</p>
<p>通过 Azure IoT，您可以实现设备的连接和管理，监控设备的状态和性能，并进行实时数据分析和预测。Azure IoT 还提供了强大的远程监控和控制功能，帮助用户实现远程设备管理和操作。</p>
<p>使用 Azure IoT，您可以构建各种物联网应用场景，如智能家居、智能工厂、智能农业和智能城市等。无论是个人用户还是企业客户，Azure IoT 都能提供可靠的物联网解决方案，帮助您实现设备的互联和数据的实时处理。</p>
<h3 id="management-governance">Management and Governance</h3>
<p>Azure Management and Governance 分类提供了一系列强大的管理和治理服务，帮助用户有效地管理和监控 Azure 资源和应用程序。</p>
<p><img alt="Management and Governance" loading="lazy" src="/zh/blog/azure-101/overview/mng-gov.png"></p>
<p>Azure Management and Governance 提供了多种管理和治理服务选项，包括资源管理、访问控制、成本管理、策略和合规性等。这些服务使用户能够轻松地管理和监控 Azure 资源和应用程序，实现资源的优化和合规性的保证。</p>
<p>通过 Azure Management and Governance，您可以实现资源的集中管理和监控，优化资源的使用和成本的控制。Azure Management and Governance 还提供了强大的访问控制和权限管理功能，帮助用户确保只有授权的用户能够访问和操作资源。</p>
<p>使用 Azure Management and Governance，您可以定义和实施策略和合规性规则，确保应用程序和数据的合规性和安全性。Azure Management and Governance 还提供了丰富的监控和报告功能，帮助用户实时了解资源的状态和性能。</p>
<h3 id="migration">Migration</h3>
<p>Azure Migration 分类提供了一系列强大的迁移服务和工具，帮助用户将现有应用程序和数据迁移到 Azure 平台上。</p>
<p><img alt="Migration" loading="lazy" src="/zh/blog/azure-101/overview/migration.png"></p>
<p>Azure Migration 提供了多种迁移服务选项，包括虚拟机迁移、数据库迁移、应用程序迁移和数据迁移等。这些服务使用户能够轻松地将现有的应用程序和数据迁移到 Azure 平台上，实现平滑的迁移过程和无缝的应用程序转换。</p>
<p>通过 Azure Migration，您可以实现虚拟机的迁移和转换，将现有的虚拟机环境迁移到 Azure 虚拟机中。Azure Migration 还提供了强大的数据库迁移功能，帮助用户将现有的数据库迁移到 Azure 数据库服务中，实现数据的平滑迁移和无缝连接。</p>
<p>使用 Azure Migration，您可以轻松地将现有的应用程序迁移到 Azure 平台上，实现应用程序的云化和弹性扩展。Azure Migration 还提供了强大的数据迁移功能，帮助用户将现有的数据迁移到 Azure 存储服务中，实现数据的安全存储和高可用性。</p>
<h3 id="mixed-reality">Mixed Reality</h3>
<p>Azure Mixed Reality 分类提供了一系列强大的混合现实服务和解决方案，用于构建、部署和管理混合现实应用程序和体验。</p>
<p><img alt="Mixed Reality" loading="lazy" src="/zh/blog/azure-101/overview/mr.png"></p>
<p>Azure Mixed Reality 提供了多种混合现实服务选项，包括混合现实开发工具、混合现实内容管理和分发、混合现实设备管理和监控等。这些服务使用户能够轻松地构建、部署和管理混合现实应用程序和体验，实现沉浸式的交互和创新的用户体验。</p>
<p>通过 Azure Mixed Reality，您可以利用先进的混合现实技术和工具来创建逼真的虚拟场景和交互体验。Azure Mixed Reality 还提供了强大的混合现实内容管理和分发功能，帮助用户将混合现实应用程序和体验推送到不同的设备和平台上。</p>
<p>使用 Azure Mixed Reality，您可以构建各种混合现实应用场景，如虚拟培训、虚拟会议、虚拟展览和虚拟旅游等。无论是个人用户还是企业客户，Azure Mixed Reality 都能提供创新的混合现实解决方案，帮助您实现沉浸式的交互和创新的用户体验。</p>
<h3 id="monitor">Monitor</h3>
<p>Azure Monitor 分类提供了一系列强大的监控和日志管理服务，帮助用户实时监测和分析应用程序和基础设施的性能和健康状况。</p>
<p><img alt="Monitor" loading="lazy" src="/zh/blog/azure-101/overview/monitor.png"></p>
<p>Azure Monitor 提供了多种监控和日志管理服务选项，包括应用程序性能监视、基础设施监视、日志分析和警报管理等。这些服务使用户能够实时监测和分析应用程序和基础设施的性能和健康状况，及时发现和解决潜在的问题。</p>
<p>通过 Azure Monitor，您可以实时监测应用程序的性能和可用性，了解应用程序的负载和响应时间。Azure Monitor 还提供了强大的基础设施监视功能，帮助用户监控服务器、虚拟机和容器等基础设施的性能和资源使用情况。</p>
<p>使用 Azure Monitor，您可以收集和分析应用程序和基础设施的日志数据，了解应用程序的运行状况和事件发生情况。Azure Monitor 还提供了灵活的警报管理功能，帮助用户设置和管理警报规则，及时通知和响应关键事件。</p>
<h3 id="networking">Networking</h3>
<p>Azure Networking 提供了一系列强大的网络服务和解决方案，用于构建、部署和管理网络基础设施和连接。</p>
<p><img alt="Networking" loading="lazy" src="/zh/blog/azure-101/overview/networking.png"></p>
<p>Azure Networking 提供了多种网络服务选项，包括虚拟网络、负载均衡、应用网关、VPN 网关和 ExpressRoute 等。这些服务使用户能够轻松地构建和管理灵活、可靠的网络架构，实现应用程序和服务之间的高速连接和通信。</p>
<p>通过 Azure Networking，您可以创建和管理虚拟网络，定义网络拓扑和子网划分，实现应用程序和服务之间的隔离和安全性。Azure Networking 还提供了强大的负载均衡和应用网关功能，帮助用户实现应用程序的高可用性和性能优化。</p>
<p>使用 Azure Networking，您可以轻松地创建和管理 VPN 网关和 ExpressRoute 连接，实现安全、可靠的远程连接和混合云环境的扩展。Azure Networking 还提供了强大的网络安全和防火墙功能，帮助用户保护网络和应用程序的安全性和隐私保护。</p>
<h3 id="security">Security</h3>
<p>在 Azure Security 分类中提供了一系列强大的安全性和合规性服务。这些服务可以帮助用户保护应用程序和数据的安全性，防止恶意攻击和数据泄露。</p>
<p><img alt="Security" loading="lazy" src="/zh/blog/azure-101/overview/security.png"></p>
<p>Azure Security 提供了多种安全性和合规性服务选项，包括身份验证和访问管理、数据保护和加密、威胁检测和防御、安全监控和审计等。这些服务提供了全面的安全性解决方案，帮助用户建立安全的云环境和应用程序。</p>
<p>通过 Azure Security，您可以实现身份验证和访问管理，保护应用程序和数据的机密性和完整性。Azure Security 还提供了强大的威胁检测和防御功能，帮助用户及时发现和应对安全威胁。</p>
<h3 id="storage">Storage</h3>
<p>Azure Storage 提供了可扩展的云存储解决方案，用于存储和管理大规模的结构化和非结构化数据。</p>
<p><img alt="Storage" loading="lazy" src="/zh/blog/azure-101/overview/storage.png"></p>
<p>Azure Storage 提供了多种存储服务选项，包括 Blob 存储、文件存储、表存储和队列存储等。这些服务使用户能够轻松地存储和访问各种类型的数据，实现数据的持久性和可靠性。</p>
<p>通过 Azure Storage，您可以存储和管理大规模的结构化和非结构化数据，如图像、视频、文档和日志等。Azure Storage 还提供了强大的数据复制和备份功能，帮助用户保护数据的安全性和可用性。</p>
<p>使用 Azure Storage，您可以实现数据的高可用性和弹性扩展，根据实际使用情况进行存储容量的调整。Azure Storage 还提供了灵活的访问控制和权限管理功能，帮助用户控制数据的访问和使用权限。</p>
<h3 id="web-mobile">Web + Mobile</h3>
<p>Azure Web + Mobile 分类提供了一系列强大的云服务和工具，用于构建、部署和管理 Web 和移动应用程序。</p>
<p><img alt="Web + Mobile" loading="lazy" src="/zh/blog/azure-101/overview/web-mobile.png"></p>
<p>Azure Web + Mobile 提供了多种服务选项，包括 Web 应用程序、移动应用程序、API 管理和通知服务等。这些服务使用户能够轻松地构建和部署高性能、可扩展的 Web 和移动应用程序。</p>
<p>使用 Azure Web + Mobile，您可以构建各种 Web 和移动应用场景，如电子商务网站、社交媒体应用、企业门户和移动客户端等。无论是个人开发者还是企业客户，Azure Web + Mobile 都能提供可靠的云服务和工具，帮助您构建和部署高性能、可扩展的 Web 和移动应用程序。</p>
<h2 id="conclusion">总结</h2>
<p>本文介绍了 Azure 101 系列课程中的 Microsoft Azure 概览。Azure 是由 Microsoft 提供的云计算平台和服务，提供了广泛的功能和服务，包括虚拟机、存储、数据库、人工智能、机器学习、区块链、物联网、容器、Serverless 计算等。Azure 还提供了全球范围的数据中心网络，使用户能够在全球范围内部署和扩展应用程序。本文还介绍了 Azure 19 个大分类的服务概述，包括 AI + 机器学习、数据分析、计算、容器、数据库、DevOps、通用云服务、混合云和多云、身份验证和访问管理、集成、物联网、管理和治理、迁移、混合现实、监控、网络、安全、存储、Web 和移动应用程序。这些服务和解决方案可以帮助用户构建、部署和管理各种类型的应用程序和服务，并提供了强大的安全性和合规性功能。无论是个人开发者、创业公司还是大型企业，Azure 都提供了灵活的定价模型，以满足不同规模和需求的项目。</p>
]]></content:encoded></item></channel></rss>