<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title><![CDATA[51学习网]]></title> 
<atom:link href="http://www.51keeplearning.com/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[51学习网]]></description>
<link>http://www.51keeplearning.com/</link>
<language>zh-cn</language>
<generator>www.emlog.net</generator>
<item>
    <title>2026年全网最深入讲解大模型备案【附：评估测试题+备案参考文件】</title>
    <link>http://www.51keeplearning.com/post-106.html</link>
    <description><![CDATA[<h2>文章目录</h2> 
<p>（一）适用主体</p> 
<p>（二）语料安全</p> 
<p>（三）模型安全</p> 
<p>（四）安全措施要求</p> 
<p>（五）词库要求</p> 
<p>（六）安全评估要求</p> 
<p>（七）附录大模型备案材料源文件</p> 
<p>2024年3月1日，我国通过了《生成式人工智能服务安全基本要求》（以下简称《AIGC安全要求》），这是目前我国第一部有关AIGC服务安全性方面的技术性指导文件，对语料安全、模型安全、安全措施、词库/题库要求、安全评估等方面提出了具体规范和要求。</p> 
<h2>（一）适用主体</h2> 
<p>《AIGC安全要求》的适用主体包括两类：</p> 
<ol> 
 <li><strong>生成式人工智能服务</strong></li> 
</ol> 
<p>定义：利用生成式人工智能技术向中华人民共和国境内公众提供生成文本、图片、音频、视频等内容的服务。</p> 
<p>其中，需要特别关注，如何认定“向境内提供服务”的范围。根据企业的实务场景，我们理解需要“从宽”去定义上述概念，包括网页端的地区选项设置有“中国”的选项、网页或产品说明提供中文选项、支付方式包含有支付宝、微信选项、注册方式允许中国手机号码等，都可能会被认定为向中国境内提供服务。</p> 
<ol> 
 <li><strong>服务提供者</strong></li> 
</ol> 
<p>定义：以交互界面、可编程接口等形式提供生成式人工智能服务的组织或个人。</p> 
<p>根据《AIGC安全要求》，个人也可成为监管对象。只要是以交互界面、可编程接口等形式提供服务，无论是组织还是个人，都符合“服务提供者”的定义。</p> 
<p><strong>（二）语料安全</strong></p> 
<p>《AIGC安全要求》针对语料安全，在语料来源授权合法、安全评估核验、不良语料类型三个方面提出了重点要求，具体要求包括：</p> 
<ol> 
 <li><strong>授权合法</strong></li> 
</ol> 
<p>语料的来源需要有合法的、明确的授权，确保其符合“授权、同意、告知”的合法性原则。根据语料的来源属性分类，具体的要求梳理如下：</p> 
<table> 
 <thead> 
  <tr> 
   <th style="text-align: left"></th> 
   <th style="text-align: left"></th> 
   <th style="text-align: left"></th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td style="text-align: left">语料来源</td> 
   <td style="text-align: left">含义</td> 
   <td style="text-align: left">合规要求</td> 
  </tr> 
  <tr> 
   <td style="text-align: left">开源语料</td> 
   <td style="text-align: left">是训练数据的主要来源，指开放的，任何人得以获取的语料</td> 
   <td style="text-align: left">应具有该语料来源的开源许可协议或相关授权文件，建议重点关注： <br> <br>（a）Robots协议； <br> <br>（b）协议明确“个人已拒绝授权采集的个人信息”。</td> 
  </tr> 
  <tr> 
   <td style="text-align: left">自采语料</td> 
   <td style="text-align: left">是指自行生产或直接从互联网采集的语料</td> 
   <td style="text-align: left">应具有采集记录，不应采集他人已明确不可采集的语料</td> 
  </tr> 
  <tr> 
   <td style="text-align: left">商业语料</td> 
   <td style="text-align: left">是自采语料的对应概念，指通过与第三方语料提供方进行交易获得的语料</td> 
   <td style="text-align: left">应有具备法律效力的交易合同、合作协议等，且当交易方或合作方不能提供语料来源、质量、安全等方面的承诺以及相关证明材料时，不应使用该语料，这就要求相关方对交易方或合作方所提供的语料、承诺、材料进行审核</td> 
  </tr> 
  <tr> 
   <td style="text-align: left">使用者输入语料</td> 
   <td style="text-align: left">是指将使用者输入的信息作为语料</td> 
   <td style="text-align: left">应具有使用者授权记录</td> 
  </tr> 
 </tbody> 
</table> 
<ol> 
 <li><strong>安全评估与核验</strong></li> 
</ol> 
<p>对于采集的语料，需要严格控制违法不良信息的比率。语料采集前，需要进行针对违法不良信息进行安全评估，采集后输入语料库之后需要进行再次核验。</p> 
<p>具体而言，如采集前评估得出违法不良信息超过5%，则该来源不得被采集；如若采集后核验违法不良信息超过5%，则该来源不得被使用。虽然两步走的设置看似天衣无缝，但从部分企业的尽调结果来看，相应管控和筛查的工作目前仍未到位。语料安全作为生产资料的重要组成部分，评估不当很可能导致紧随其后的产品研发环节就出现问题。</p> 
<p>除此以外，还需要遵循在内容过滤、知识产权、个人信息、标注安全等方面的合规要求：<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202412/3456369-20241225141013012-1525928707.png"></p> 
<p>*图示：语料来源合规要求</p> 
<p>因此，企业需要在首次安全评估之后保持对语料来源的持续敏感度。并在之后的操作处理中进行持续性的内容过滤，采取关键词、分类模型、人工抽检等方式过滤违法不良信息。对于涉及知识产权和个人信息的语料，还应当设置专门的负责人和管理策略，在使用之前注意该语料是否存在侵犯他人权利的情况，并与相关方提前协商，告知有关风险或取得其授权同意，并取得正式性的记录文件；要求语料提供方提供语料来源、质量安全等承诺以及相关证明材料并进行审核。</p> 
<ol> 
 <li><strong>不良语料类型</strong></li> 
</ol> 
<p>对于具体需要规避的语料类型，《AIGC安全要求》附录A列举了涉及语料及生成内容安全的类型，共分为5类31种，制定了特别的安全需求：<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202412/3456369-20241225141023389-1320497911.png"></p> 
<p>*图示：不良语料类型</p> 
<p>需要特别关注的是，本次不良语料类型特别提出了针对“特定服务类型”的安全需求。</p> 
<p>例如，针对医疗建议类的算法和问答建议，当前不少医疗健康产品和服务当中，开发了智能健康管家等功能，涉及根据用户的健康分析报告，提出对应的诊疗分析、用药建议等，对于这一类功能，需要特别谨慎对待其是否可能出现非专业性、误导性的用药和诊断结论，否则可能导致“无病呻吟”或“病急乱投医”的情况。</p> 
<p>例如，针对金融投资建议类的算法和分析建议，需要遵循银行、保险、金融等方面对于投资者、投保人的合法权益保护，避免违反相关的监管规定，为了业务竞争而向投资者、投保人等通过AI作出涉及不正当竞争、违反金融风险管控的分析或违规引导。</p> 
<h2>（三）模型安全</h2> 
<p>除语料安全外，模型安全也是AIGC服务或服务提供者应当考虑的重要因素。提供者不仅应当在其服务过程中，提供安全、稳定、持续的服务，保障用户的正常使用；如若需要基于第三方基础模型提供服务，提供者还必须使用已经主管部门备案的基础模型。<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202606/3456369-20260625162943111-1375588792.png"></p> 
<p>*统计：截至2026年6月通过网信办备案的算法模型情况</p> 
<p>而针对AIGC模型产出的内容，也应从内容的安全性、准确性和可靠性三个角度予以关注：</p> 
<p>首先在安全性方面，《AIGC安全要求》规定应当对每次对话使用者输入的信息进行安全性检测，引导模型生成积极正向内容，并建立常态化检测测评手段，针对发现的安全问题及时整改；其次，在准确性方面，应采取技术措施提高生成内容响应使用者输入意图的能力，以及生成内容与科学常识和主流认知的符合程度；最后，在可靠性方面，也应采取技术措施提高生成内容格式框架的合理性和有效内容含量。</p> 
<h2>（四）安全措施要求</h2> 
<p>在适用模型和输出内容的过程中，AIGC相关主体还应当满足《AIGC安全要求》对于“模型适用安全措施”的规定，及时建立起与技术安全要求配套的技术措施、管理措施和相应保护措施。<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202412/3456369-20241225141044663-1387905767.png"></p> 
<p>安全措施要求</p> 
<ol> 
 <li><strong>Opt-out功能</strong></li> 
</ol> 
<p>其中，需要重点关注的是“Opt-out”机制的建立要求。不同于我国目前一般采用的“Opt-in”机制（即用户自主选择是否提供信息，不得默认设置为提供），当前在AIGC的信息输入节点，仅要求提供显著的、便捷的供使用者关闭信息输入的路径即可，需要保障使用者可以随时方便关闭信息输入的功能。</p> 
<ol> 
 <li><strong>供应链安全</strong></li> 
</ol> 
<p>在全球化的贸易背景下，包括GPU芯片在内的半导体技术已成为科技领域的核心竞争力。GPU芯片涉及AI模型训练、数据中心、智能交通、游戏等多个下游行业，这些芯片根据应用场景又细分为云端芯片和终端芯片，其中终端芯片广泛应用于嵌入式系统、移动设备、智能制造和家居智能等细分领域。</p> 
<p>因此，在《AIGC安全要求》中，重点突出了芯片、软件、工具、算力等方面在供应链安全方面的合规要求，也是对于中国企业在采购、研发、使用、出口相关服务和产品的同时，需要从业务连续性、稳定性的角度，做好对供应链方面的风险排查，避免因对于业务所在国家地区管制规则要求的不熟悉而引发市场准入，甚至是直接影响业务的风险。</p> 
<h2>（五）词库要求</h2> 
<p>《AIGC安全要求》在词库方面要求设立关键词库、生成内容测试题库、拒答测试题库和分类模型等。其中，关键词库的总规模不少于10000个关键词，且关键词库应至少覆盖《AIGC安全要求》附录A.1以及A.2中的17种安全风险，附录A.1中每一种安全风险的关键词均不宜少于200个，附录A.2中每一种安全风险的关键词均不宜少于100个。这些模型对于语料内容过滤，生成内容安全评估等，应完整覆盖本文件附录A中全部31种安全风险。</p> 
<h2>（六）安全评估要求</h2> 
<p>针对最后一项安全评估要求，《AIGC安全要求》第九章内容对评估的方法、语料评估、生成内容评估和问题解答评估都作出了规定，这也是对于《AIGC暂行办法》的细化。</p> 
<p>评估方法论层面，可以由服务提供者自行开展，也可委托给第三方评估机构；对于符合的评估结果应保存充分的证明材料，对于不符合的应说明不符合或不适用的原因并对后续改进措施进行计划说明；对于最后形成的评估报告，除了应有各项和总项的结论之外，还应有3位以上负责人签字。</p> 
<p>而对于细化层面的安全评估，《AIGC安全要求》确立了如要求语料安全评估的人工抽检和技术抽检的标准，生成内容安全评估的人工抽检、关键词抽检和分类模型抽检的标准，以及问题拒答评估的应拒答测试题库和非拒答测试题库的标准等。<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202412/3456369-20241225141054209-964910500.png"></p> 
<p>*图示：AIGC安全评估要求</p> 
<h2>（七）附录、大模型备案材料源文件</h2> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202606/3456369-20260625163145325-323895562.png"></p> 
<p>大模型备案材料提交版本</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202412/3456369-20241225141228142-1901067677.png"></p> 
<p>大模型备案&amp;算法备案参考材料</p> 
<h2>备案建议</h2> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/3456369/202412/3456369-20241225141243012-2068340029.jpg"></p> 
<p>如果企业能自己做，可以考虑安排产品、算法、法务等团队成员一起完成大模型备案、算法备案，毕竟找第三方公司协助也是一笔不少的支出费用，如果自己办理有困难，也欢迎联系交流。</p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:49 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-106.html</guid>
</item>
<item>
    <title>模型训练入门教程：从MNIST手写数字迁移到EMNIST手写字母的识别之路</title>
    <link>http://www.51keeplearning.com/post-105.html</link>
    <description><![CDATA[<blockquote> 
 <p>很多人入门深度学习，第一步就是复制 <code>GitHub</code> 上的 <code>MNIST</code> 代码，跑通，看到 99% 的准确率，然后就以为自己会 <code>CNN</code> 了。</p> 
 <p>我也一样。直到我开始问自己一些“较真”的问题，才发现：<strong>代码能跑 ≠ 我真的懂</strong>​</p> 
 <p>这篇教程，记录我从调包、解剖、设计，再到踩完迁移学习坑的全过程。没有玄学，只有一步一步的脚踏实地。</p> 
</blockquote> 
<hr> 
<p>本文首先从<code>MNIST</code>的训练过程开始讲起，会详细讲解过程和步骤，会有图例和代码，以及一些踩到的坑等注意事项。下面先介绍一些前置要点：</p> 
<h2>前言</h2> 
<h3><strong>深度学习框架</strong>：PyTorch</h3> 
<p><code>PyTorch</code>是一个开源的深度学习框架，由 Facebook 的人工智能研究团队开发，广泛应用于计算机视觉（CV）、自然语言处理（NLP）等领域。它以灵活性和易用性著称，特别适合研究人员和开发者进行快速原型设计和实验。</p> 
<blockquote> 
 <p>PyTorch 的模型训练通常包括以下步骤：</p> 
</blockquote> 
<ol> 
 <li> <p><strong>数据加载</strong>：使用&nbsp;<em>Dataset</em>&nbsp;和&nbsp;<em>DataLoader</em>&nbsp;加载和预处理数据。</p> </li> 
 <li> <p><strong>模型定义</strong>：通过继承&nbsp;<em>torch.nn.Module</em>&nbsp;构建神经网络。</p> </li> 
 <li> <p><strong>损失函数与优化器</strong>：选择合适的损失函数（如交叉熵）和优化器（如 SGD 或 Adam）。</p> </li> 
 <li> <p><strong>训练与验证</strong>：通过循环迭代训练模型，并使用验证集评估性能。</p> </li> 
 <li> <p><strong>保存与加载模型</strong>：使用&nbsp;<em>torch.save</em>&nbsp;和&nbsp;<em>torch.load</em>&nbsp;保存和恢复模型。</p> </li> 
</ol> 
<h3><strong>深度学习模型</strong>：CNN</h3> 
<p><code>CNN</code>（卷积神经网络），适合用在图像识别和分类中，通过一系列的<code>卷积层</code>、<code>池化层</code>和<code>全连接层</code>来处理数据</p> 
<p>在卷积层中，<code>CNN</code>使用一组可学习的滤波器（卷积核）来<strong>扫描输入</strong>的图像或信号。每个滤波器都能够检测输入中的特定特征，如边缘或颜色斑点。通过这种方式，CNN能够捕捉到图像中的<strong>局部特征</strong>，并保持这些特征的空间关系。</p> 
<p>池化层（也称为下采样层）则用于<strong>降低</strong>特征的空间尺寸，从而减少参数数量和计算复杂度，同时使特征检测更加鲁棒。全连接层则将学习到的高级特征用于<strong>分类</strong>或其他任务。</p> 
<blockquote> 
 <p>CNN的结构和工作原理</p> 
</blockquote> 
<p>一个典型的CNN包含以下几个主要部分：</p> 
<ul> 
 <li> <p><strong>输入层</strong>：接收原始数据，如图像的像素值。</p> </li> 
 <li> <p><strong>卷积层</strong>：使用多个卷积核提取输入的特征。</p> </li> 
 <li> <p><strong>激活函数</strong>：如ReLU，用于引入非线性，使网络能够学习更复杂的特征。</p> </li> 
 <li> <p><strong>池化层</strong>：降低特征的空间维度，减少计算量。</p> </li> 
 <li> <p><strong>全连接层</strong>：将学习到的特征映射到最终的输出，如分类标签。</p> </li> 
 <li> <p><strong>输出层</strong>：输出网络的最终结果，如分类的概率分布。</p> </li> 
</ul> 
<p>CNN通过这些层的堆叠，能够从简单到复杂逐渐提取图像的特征。在训练过程中，CNN通过<strong>反向传播算法</strong>调整卷积核中的权重，以<strong>最小化</strong>预测结果和真实标签之间的差异。</p> 
<h2>训练MNIST</h2> 
<blockquote> 
 <p>什么是MNIST</p> 
</blockquote> 
<p>全称：（Modified National Institute of Standards and Technology），是机器学习和深度学习领域最经典的入门数据集，被称为<strong>深度学习的“Hello World”</strong>。它包含<strong>手写数字（0-9）</strong> 的灰度图像，广泛用于图像分类算法的训练与测试。</p> 
<h3>模型定义</h3> 
<p>首先定义模型的结构，这个结构在训练和推理过程中都是要保持一致的，其中包括模型组件的定义和模型流程定义，代码如下所示：</p> 
<pre><code>class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout2d(0.25)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output
</code></pre> 
<p>上面的代码中，首先定义了模型的组件结构，其次<code>forward</code>是模型的流程结构。这是一个经典的“<code>特征提取</code> + <code>分类器</code>”双阶段结构，分为两个阶段。</p> 
<p>第一阶段：特征提取（卷积神经网络），负责把像素变成“语义特征”。</p> 
<ul> 
 <li> <p>卷积层 1(Conv1)：初级特征提取（边缘、线条）</p> 
  <ul> 
   <li><code>self.conv1 = nn.Conv2d(1, 32, 3, 1)</code></li> 
   <li>in_channels=1 : 输入是灰度图（1个通道）</li> 
   <li>out_channels=32: 派出32个不同的“侦探”（滤波器）</li> 
   <li>kernel_size=3 : 每个侦探拿3x3的放大镜</li> 
   <li>stride=1 : 每次移动1个像素</li> 
   <li>输出尺寸计算：(28 - 3 + 1) = 26 -&gt; 输出 (32, 26, 26)</li> 
  </ul> </li> 
 <li> <p>卷积层 2：高级特征提取（部件、纹理）</p> 
  <ul> 
   <li><code>self.conv2 = nn.Conv2d(32, 64, 3, 1)</code></li> 
   <li>in_channels=32: 接收上一层的32个特征图</li> 
   <li>out_channels=64: 增加到64个侦探，组合更复杂的模式</li> 
   <li>输出尺寸计算：(26 - 3 + 1) = 24 -&gt; 输出 (64, 24, 24)</li> 
  </ul> </li> 
 <li> <p>Dropout 层 1：针对卷积特征的正则化</p> 
  <ul> 
   <li><code>self.dropout1 = nn.Dropout2d(0.25)</code></li> 
   <li>作用：随机“掐断”25%的通道（整张特征图）,强迫模型不要把赌注押在某几个特定的特征组合上</li> 
   <li>为什么用 Dropout2d：防止特征图之间产生共适应（Co-adaptation）</li> 
  </ul> </li> 
 <li> <p>Dropout 层 2：针对全连接层的正则化</p> 
  <ul> 
   <li><code>self.dropout2 = nn.Dropout2d(0.5)</code></li> 
   <li>作用：随机“掐断”50%的神经元，因为全连接层参数最多，最容易过拟合</li> 
  </ul> </li> 
</ul> 
<p>第二阶段：分类器（全连接网络），负责把特征变成具体的数字类别</p> 
<ul> 
 <li> <p>全连接层 1：特征映射与降维</p> 
  <ul> 
   <li><code>self.fc1 = nn.Linear(9216, 128)</code></li> 
   <li>输入维度 9216 的由来（关键计算）</li> 
   <li>经过 Conv2(64, 24, 24) -&gt; MaxPool(2, 2)</li> 
   <li>输出尺寸：64个通道 * 12 * 12 = 9216</li> 
   <li>输出维度 128：将高维特征压缩成128维的“数字指纹”</li> 
  </ul> </li> 
 <li> <p>全连接层 2：最终输出层</p> 
  <ul> 
   <li><code>self.fc2 = nn.Linear(128, 10)</code></li> 
   <li>输入 128：接收上层的指纹</li> 
   <li>输出 10：对应 0-9 十个数字类别</li> 
  </ul> </li> 
</ul> 
<p>而这些阶段落实到的流程执行中，需要用到一些额外的手段，如下所示：</p> 
<ul> 
 <li><code>relu</code>，激活函数：引入非线性，筛选有效特征</li> 
 <li><code> max_pool2d</code>，最大池化，模型不再关心像素的精确位置，只关心“有没有”</li> 
 <li><code>flatten</code>，展平：把三维特征图拉直成一维向量，为了对接全连接层（只能吃一维数据）</li> 
 <li><code>log_softmax</code>，转换为对数概率</li> 
</ul> 
<p><code>forward</code>函数清晰地展示了数据流(Flow)：<strong>像素 → 边缘 → 部件 → 抽象特征 → 概率</strong>。</p> 
<blockquote> 
 <p> 我当时真实的困惑</p> 
</blockquote> 
<ol> 
 <li><strong>为什么卷积核数量是 <code>32</code>和 <code>64</code>？不能是 <code>30</code>吗？</strong></li> 
 <li><strong><code>9216</code>这个数字是怎么来的？</strong></li> 
 <li><strong>forward 里，数据尺寸到底是怎么一步步变化的？</strong></li> 
</ol> 
<p>这几个是非常典型、也非常值得认真回答的问题。</p> 
<p>首先回答第一个问题，<strong>可以是 30，但几乎没人这么做。</strong></p> 
<p>原因有三个，都是工程现实，不是玄学：</p> 
<p>1️⃣ 最重要的是GPU 硬件对齐，因为现代 GPU 的运算单元（CUDA Core / Tensor Core）是<strong>按 2 的幂次并行</strong>工作的，能刚好对齐显存带宽和线程块（block），如果用 <code>30</code>会出现 <strong>padding / waste</strong>，计算效率下降，训练变慢。</p> 
<p>2️⃣ 经验性容量设计，在第一层提取简单特征（边、角、点），<code>32</code>个通道足够覆盖常见低级模式；第二层组合成复杂结构（弧线、圈、部件），需要更多表示能力，<code>64</code>是实践中验证过的稳定选择</p> 
<p>3️⃣ 历史惯性（LeNet 传统），很多教程只是继承这种 <strong>翻倍增长</strong>​ 的设计，而不是重新发明</p> 
<p>其次是第二个问题，9216 不是玄学，是尺寸推导，是<strong>算出来的</strong></p> 
<h3> 已知前提</h3> 
<ul> 
 <li>输入图片：<code>28 × 28</code></li> 
 <li>卷积核：<code>3 × 3</code></li> 
 <li>padding：<code>0</code></li> 
 <li>stride：<code>1</code></li> 
</ul> 
<h3> 每一层尺寸变化</h3> 
<table> 
 <thead> 
  <tr> 
   <th>层</th> 
   <th>操作</th> 
   <th>输出尺寸</th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td>Input</td> 
   <td>原始图片</td> 
   <td>1 × 28 × 28</td> 
  </tr> 
  <tr> 
   <td>Conv1</td> 
   <td>3×3 卷积，无 padding</td> 
   <td>32 × 26 × 26</td> 
  </tr> 
  <tr> 
   <td>MaxPool</td> 
   <td>2×2 池化</td> 
   <td>32 × 13 × 13</td> 
  </tr> 
  <tr> 
   <td>Conv2</td> 
   <td>3×3 卷积</td> 
   <td>64 × 11 × 11</td> 
  </tr> 
  <tr> 
   <td>MaxPool</td> 
   <td>2×2 池化</td> 
   <td>64 × 5 × 5</td> 
  </tr> 
 </tbody> 
</table> 
<p>️ 注意：不同实现可能略有差异，但常见版本最后会再接一个卷积或 padding，使最终特征图为 <code>64 × 12 × 12</code>。</p> 
<p>那么最终，9216 的来源就是</p> 
<pre><code>64 个通道
× 12 × 12 每个通道的特征图
= 9216
</code></pre> 
<h3>模型训练</h3> 
<p>整体来说，训练过程就是把一个 batch 的数据送进模型 → 算损失 → 反向传播 → 更新参数 → 记录日志，这是深度学习里“学习”发生的唯一地方</p> 
<blockquote> 
 <p> <strong>PyTorch 的数据是“流式”的，不是一次性全塞进去</strong></p> 
</blockquote> 
<pre><code>def train(model, device, train_loader, optimizer, epoch, losses):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)

        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()

        losses.append(loss.item())

        if batch_idx % 100 == 0:
            print(
                f'Epoch {epoch} '
                f'[{batch_idx * len(data)}/{len(train_loader.dataset)} '
                f'({100. * batch_idx / len(train_loader):.0f}%)]\t'
                f'Loss: {loss.item():.6f}'
            )
</code></pre> 
<p>函数参数表含义如下所示：</p> 
<table> 
 <thead> 
  <tr> 
   <th><strong><br><br>参数<br><br></strong></th> 
   <th><strong><br><br>是什么<br><br></strong></th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td><code>model</code></td> 
   <td>你的 CNN 网络</td> 
  </tr> 
  <tr> 
   <td><code>device</code></td> 
   <td>CPU / GPU</td> 
  </tr> 
  <tr> 
   <td><code>train_loader</code></td> 
   <td>数据加载器（自动分批）</td> 
  </tr> 
  <tr> 
   <td><code>optimizer</code></td> 
   <td>优化器（Adam / SGD）</td> 
  </tr> 
  <tr> 
   <td><code>epoch</code></td> 
   <td>当前是第几轮</td> 
  </tr> 
  <tr> 
   <td><code>losses</code></td> 
   <td>用来记录 loss 的列表</td> 
  </tr> 
 </tbody> 
</table> 
<p>首先要做的就是让模型切换到训练模式，使用<code>model.train()</code>，它告诉模型Dropout层开启随机失活，BatchNorm层使用当前batch 的均值/方差</p> 
<p>如果有GPU，那么<code>data, target = data.to(device), target.to(device)</code>就是把数据搬到GPU</p> 
<p>因为PyTorch <strong>默认梯度是累加的</strong>，所以使用<code>optimizer.zero_grad()</code>清空梯度，来给新的学习腾地方</p> 
<p>而模型在这里的作用就是做前向传播，就是前面所将的流程：</p> 
<pre><code>Conv1 → ReLU → Conv2 → ReLU → Pool → Dropout → FC → Softmax 
</code></pre> 
<p>模型的输出output的形状是<code>(batch_size, 10)</code></p> 
<p>计算损失使用<code>loss = F.nll_loss(output, target)</code>，它衡量的是模型猜的有多离谱，其中<code>nll_loss</code>是负对数似然，target是真实标签（0~9），我们肯定是想要<strong>最小化损失函数</strong>的</p> 
<p>反向传播（Backward Pass）是<strong>深度学习最核心的一行代码</strong>，可以自动计算<span>\(\frac{∂Loss}{∂w}\)</span>，然后把<br> 梯度存到每个参数的 <code>.grad</code>属性里，核心是把“总错误”按责任比例分摊回去，对比正向传播，过程如下：</p> 
<pre><code>Loss
 ↓
∂Loss/∂FC
 ↓
∂FC/∂Pool
 ↓
∂Pool/∂Conv2
 ↓
∂Conv2/∂Conv1
 ↓
∂Conv1/∂W
</code></pre> 
<blockquote> 
 <p>有反向传播就可以自动学习，而不用手动调参</p> 
</blockquote> 
<p>梯度告诉了方向，然后是优化器（Optimizer Step）<code>optimizer.step()</code>负责迈步子，读取<code>.grad</code>，根据优化规则更新参数，其中Adam 做的是：<span>\(\theta=\theta-η⋅ \frac{m}{\sqrt{ v }+ϵ}\)</span>，这是“真正动手改模型”的地方</p> 
<p>大致的训练流程就是这样，那么现在开始实际的训练吧。</p> 
<pre><code>def main():
    # ==========================================
    # 1. 设备选择与配置
    # ==========================================
    # 自动检测是否有可用的 GPU（CUDA）
    # 如果有则用 GPU 加速训练，否则回退到 CPU
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # ==========================================
    # 2. 模型与优化器初始化
    # ==========================================
    # 实例化我们定义的 CNN 模型
    # .to(device) 将模型的所有参数和缓冲区移动到指定设备（GPU/CPU）
    model = Net().to(device)

    # 使用 Adam 优化器
    # model.parameters() 告诉优化器：需要更新哪些参数
    # lr=0.001 是学习率，控制每次参数更新的步长
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    # ==========================================
    # 3. 数据预处理管道
    # ==========================================
    # transforms.Compose 将多个变换组合成一个流水线
    transform = transforms.Compose([
        # 将 PIL 图像或 numpy.ndarray 转换为 PyTorch Tensor
        # 同时将像素值从 [0, 255] 缩放到 [0.0, 1.0]
        transforms.ToTensor(),

        # 标准化处理（Normalization）
        # 使用 MNIST 数据集的官方均值和标准差
        # 公式: output = (input - mean) / std
        # 作用: 使数据分布均值为0，方差为1，加速模型收敛
        transforms.Normalize((0.1307,), (0.3081,))
    ])

    # ==========================================
    # 4. 数据集与数据加载器
    # ==========================================
    # 加载 MNIST 训练数据集
    dataset = datasets.MNIST(
        './data',          # 数据存储路径
        train=True,        # 使用训练集（而非测试集）
        download=True,     # 如果数据不存在，自动从网上下载
        transform=transform  # 应用上面定义的数据预处理
    )

    # DataLoader 负责批量加载数据，并提供打乱、并行读取等功能
    train_loader = DataLoader(
        dataset,           # 要加载的数据集
        batch_size=64,     # 每个批次包含 64 个样本
        shuffle=True       # 每个 epoch 都打乱数据顺序（防止模型记忆顺序）
    )

    # ==========================================
    # 5. 训练循环
    # ==========================================
    # 用于记录每个 batch 的损失值，以便后续可视化
    losses = []

    # 训练 10 个 epoch（完整遍历数据集 10 次）
    for epoch in range(1, 11):
        # 调用我们定义的 train 函数
        # 将模型、设备、数据加载器、优化器、当前轮次和损失列表传入
        train(model, device, train_loader, optimizer, epoch, losses)

    # ==========================================
    # 6. 保存训练好的模型
    # ==========================================
    # 保存模型的参数（state_dict）
    # 只保存参数而不保存整个模型是推荐做法，节省空间且灵活
    torch.save(model.state_dict(), "mnist_cnn.pt")

    # ==========================================
    # 7. 可视化训练过程
    # ==========================================
    # 创建一个 8x5 英寸大小的画布
    plt.figure(figsize=(8, 5))

    # 绘制损失曲线
    # losses 列表记录了每个 batch 的损失值
    plt.plot(losses, label="Training Loss")

    # 设置坐标轴标签
    plt.xlabel("Batch")      # X轴：批次索引
    plt.ylabel("Loss")       # Y轴：损失值

    # 设置图表标题
    plt.title("MNIST CNN Training Loss")

    # 显示网格线，便于观察数值
    plt.grid(True)

    # 显示图例（对应 label="Training Loss"）
    plt.legend()

    # 自动调整子图参数，使之填充整个画布
    plt.tight_layout()

    # 显示图形窗口
    plt.show()
</code></pre> 
<p>经过训练循环过程后，模型的犯错程度在不断的下降，如下图所示：</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172318465-1341255342.png"></p> 
<p>到这里，基本的训练过程就讲差不多了，那么一个epoch 里到底发生了什么？</p> 
<pre><code>Epoch 1
 ├─ Batch 1: 猜 → 算错 → 改参数
 ├─ Batch 2: 猜 → 算错 → 改参数
 ├─ ...
 └─ Batch N: 猜 → 算错 → 改参数

Epoch 2
 ├─ 猜得更准一点
 ├─ Loss 更小
 └─ ...

Epoch 10
 └─ 基本稳定
</code></pre> 
<blockquote> 
 <p>训练函数的本质，就是用损失函数告诉模型“你错在哪，再用反向传播告诉它“该怎么改”</p> 
</blockquote> 
<h3>模型测试</h3> 
<p>我使用了Forward Hook（前向钩子）方法，来达到可视化推理的中间过程，如下所示：</p> 
<pre><code>features = {}
def hook(name):
    def fn(_, __, out):
        features[name] = out.detach()
    return fn

# 注册钩子
model.conv1.register_forward_hook(get_features('conv1'))
model.conv2.register_forward_hook(get_features('conv2'))
model.dropout1.register_forward_hook(get_features('dropout1'))
</code></pre> 
<p>进行测试时，加载之前训练好的模型权重，并开启<code>model.eval()</code>，同时需要注意<strong>模型定义要和训练时保持一致</strong></p> 
<pre><code>device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = Net().to(device)
model.load_state_dict(torch.load("mnist_cnn.pt", map_location=device))
model.eval()
</code></pre> 
<p>模型接受的输入需要先预处理一下，因为MNIST的图片有一定规范，比如尺寸、颜色等</p> 
<pre><code># ============ 图片预处理 ===============
img_path = "my_digit.png"
img = Image.open(img_path).convert("L")

#  白底黑字 → 黑底白字（MNIST 风格）
img = ImageOps.invert(img)

# resize 到 28×28
img = img.resize((28, 28), Image.LANCZOS)

# 输入预处理
transform = T.Compose([
    T.ToTensor(),
    T.Normalize((0.1307,), (0.3081,))
])
# 获得模型输入
input_tensor = transform(img).unsqueeze(0).to(device)
</code></pre> 
<p>最后就是模型推理代码了</p> 
<pre><code># ==========================================
# 推理模式（禁用梯度计算）
# 作用：节省显存、加快计算、关闭 Dropout
# ==========================================
with torch.no_grad():

    # ==========================================
    # 前向传播：让模型看这张图片
    # output 形状: (1, 10)
    # 注意：这是 log_softmax 输出（对数概率）
    # ==========================================
    output = model(input_tensor)

    # ==========================================
    # 将对数概率转换回普通概率
    # 公式: prob = exp(log_prob)
    # 作用：让人类能直观理解（0~1 之间）
    # ==========================================
    prob = torch.exp(output)

    # ==========================================
    # 找出概率最大的类别（预测结果）
    # argmax(dim=1): 在类别维度上找最大值索引
    # item(): 把 Tensor 转成 Python 整数
    # 结果: 0~9 的数字
    # ==========================================
    pred = prob.argmax(dim=1).item()

    # ==========================================
    # 取出该类别的预测置信度
    # prob[0, pred]: 第 0 个样本、预测类别上的概率值
    # item(): 转成 Python 浮点数
    # 结果: 0.0~1.0 之间的置信度
    # ==========================================
    conf = prob[0, pred].item()
</code></pre> 
<p>使用上面提到的钩子函数获取过程特征图，以<code>Conv1</code>举例：</p> 
<pre><code>conv1_feat = features['conv1'].squeeze().cpu()

plt.figure(figsize=(12, 6))
for i in range(32):
    plt.subplot(4, 8, i+1)
    plt.imshow(conv1_feat[i], cmap='gray')
    plt.axis('off')
plt.suptitle("Conv1: Edge Detectors")
plt.show()
</code></pre> 
<p>下面就是推理的中间过程图片，有4个，分别是<code>conv1</code>,<code>conv2</code>,<code>dropout1</code>以及全连接层的输出，如下所示：</p> 
<ol> 
 <li><strong>Conv1 输出</strong></li> 
</ol> 
<p>32 张特征图，有的对竖边敏感，有的对横边敏感，有的对角点敏感<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172338772-1669753786.png"></p> 
<ol> 
 <li><strong>Conv2 输出</strong></li> 
</ol> 
<p>不再是简单边缘，开始出现：圈、弧线、局部形状<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172346882-32693380.png"></p> 
<ol> 
 <li>Pooling + Dropout输出</li> 
</ol> 
<p>池化在“做减法”（精简信息），Dropout 在“做干扰”（增强韧性）<br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172355345-271209860.png"></p> 
<ol> 
 <li><strong>全连接层</strong></li> 
</ol> 
<p>抽象成 10 个类别的概率分布，这10个概率来源于<code>torch.exp(output)</code><br> <img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172402239-1385223803.png"></p> 
<p>整个过程中，CNN 真的是在<strong>从像素 → 边缘 → 部件 → 类别</strong>。</p> 
<h3>踩坑实录</h3> 
<p> 真实踩坑</p> 
<table> 
 <thead> 
  <tr> 
   <th>问题</th> 
   <th>现象</th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td>OpenMP 报错</td> 
   <td><code>libiomp5md.dll</code>冲突</td> 
  </tr> 
  <tr> 
   <td>CUDA 报错</td> 
   <td>Tensor 不能直接转 NumPy</td> 
  </tr> 
  <tr> 
   <td>识别不准</td> 
   <td>字太大 / 太小 / 位置偏</td> 
  </tr> 
 </tbody> 
</table> 
<p> 解决方案</p> 
<p>1️⃣ DLL 冲突（Windows）</p> 
<pre><code>os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
</code></pre> 
<p>2️⃣ GPU → CPU → NumPy</p> 
<pre><code>tensor.cpu().detach().numpy()
</code></pre> 
<p>3️⃣ 图片预处理（关键点）</p> 
<pre><code>img = Image.open(img_path).convert("L")  # 灰度
img = ImageOps.invert(img)               # 黑字白底 → 白字黑底
img = img.crop(bbox)                    # 去白边
img = img.resize((28, 28))              # 固定尺寸
</code></pre> 
<p><strong>结论</strong>​</p> 
<p> <strong>90% 的问题不在模型，而在数据。</strong></p> 
<h2>迁移训练EMNIST</h2> 
<p>原本我并不想训练<code>EMNIST</code>，我想要训练自己的数据集，具体一点是想要在已有模型基础上，增加新的内容，也就是<code>迁移训练</code>。</p> 
<p>我辛辛苦苦手搓了大小写字母的手写图片(鼠标画的)，每个字母有3张，总共156张图片，分别保存在以大小写字母命名的文件夹目录内。</p> 
<p>第一次尝试，我以为只是把 <code>10</code>改成 <code>62</code>，结果：<strong>数字准确率从 99% → 10%，</strong> 以失败告终，但也让我认识到数据源的重要性，我认知到156 张字母图太少了，解冻卷积层后，模型把数字特征全忘了，这是发生了<strong>灾难性遗忘</strong></p> 
<p>然后我开始转为<code>EMNIST</code>数据集，并且沿用训练<code>MNIST</code>时的模型。从 MNIST 到 EMNIST 的升级过程也是悲伤的，发现效果很差，准确率一直在<code>49%</code>徘徊，最后排查出来有如下几个原因：</p> 
<ul> 
 <li><strong>迁移学习</strong>策略错误：MNIST 只有 <strong>10 类</strong>，而EMNIST 有 <strong>62 类</strong>，加载了 MNIST 权重直接训练会导致<strong>梯度冲突</strong></li> 
 <li>模型容量不足：训练MNIST时，特征图尺寸变化为：Conv(32) → Conv(64) → FC(128) → FC(62)，对于 62 类手写字符，<strong>FC(128) 太小了</strong></li> 
</ul> 
<p>所以我修改了模型定义以及迁移学习策略，把模型容量提升；迁移学习时不要一次性训练所有层，分阶段进行训练；最后进行数据增强，添加一些随机旋转和平移等干扰。</p> 
<h3>模型定义</h3> 
<p>对比训练<code>MNIST</code>的模型定义，只是增加了模型容量，及<code>conv1</code>,<code>conv2</code>,<code>fc1</code>,<code>fc2</code>的尺寸，前向传播过程保持不变，代码如下所示：</p> 
<pre><code>class Net(nn.Module):
    def __init__(self, num_classes=62):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 64, 3, 1)
        self.conv2 = nn.Conv2d(64, 128, 3, 1)
        self.dropout1 = nn.Dropout2d(0.25)
        self.dropout2 = nn.Dropout2d(0.5)
        self.fc1 = nn.Linear(128 * 12 * 12, 256)
        self.fc2 = nn.Linear(256, num_classes)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = F.relu(self.fc1(x))
        x = self.dropout2(x)
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)
</code></pre> 
<h3>模型训练</h3> 
<p>首先加载数据，以及输入数据的预处理定义</p> 
<pre><code># 模型输入数据预处理，进行了数据增强
transform_train = transforms.Compose([
    transforms.RandomRotation(90),  #  关键！允许 ±90° 旋转
    transforms.RandomAffine(
        degrees=15,
        translate=(0.1, 0.1),
        scale=(0.8, 1.2)
    ),
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

# 加载EMNIST数据集
train_dataset = datasets.EMNIST(
    './data', split='byclass', train=True, download=True, transform=transform_train
)

# 定义数据加载器，决定怎么给数据
train_loader = DataLoader(
    train_dataset,
    batch_size=128,        # 从 64 → 128
    shuffle=True,
    num_workers=4,        # CPU 核心数
    pin_memory=True,      # GPU 加速
    persistent_workers=True
)
</code></pre> 
<p>然后加载已训练好的<code>MNIST</code>权重，并且仅加载卷积层</p> 
<pre><code>mnist_weights = torch.load('mnist_cnn.pt', map_location=device)
conv_dict = {k: v for k, v in mnist_weights.items() 
             if 'conv' in k and v.shape == model.state_dict()[k].shape}
model_dict = model.state_dict()
model_dict.update(conv_dict)
model.load_state_dict(model_dict)
</code></pre> 
<p>接下来就是分阶段训练的内容了，主要分为两个阶段，第一阶段是冻结其他层，只训练分类头(全连接层)，以保证已学习的特征不会被破坏，这是迁移学习常用的方式。第二个阶段就是微调整个模型，对预训练模型的参数进行调整，使模型更好地适应新的内容。</p> 
<pre><code>def train_stage(stage, epochs, lr, train_all=False):
    """
    执行一个训练阶段（stage-wise training）

    参数说明：
    stage       : 当前是第几个训练阶段（仅用于打印）
    epochs      : 该阶段要训练的 epoch 数
    lr          : 学习率
    train_all   : 是否训练所有层
                  False → 只训练分类头（迁移学习常用）
                  True  → 微调整个模型
    """

    # =========================
    # 1. 打印当前训练阶段信息
    # =========================
    print(f"\n{'='*50}")
    print(f"Stage {stage}: {'All layers' if train_all else 'Only classifier'}")
    print(f"{'='*50}")

    # =========================
    # 2. 控制哪些层参与训练
    # =========================
    if not train_all:
        # 2.1 先冻结整个模型的所有参数
        for param in model.parameters():
            param.requires_grad = False

        # 2.2 只解冻分类头（全连接层）
        for param in model.fc1.parameters():
            param.requires_grad = True
        for param in model.fc2.parameters():
            param.requires_grad = True

        #  目的：
        # 保护卷积层已经学到的通用特征（边缘、形状）
        # 防止小数据集导致“灾难性遗忘”
    else:
        # 2.3 微调整个模型
        for param in model.parameters():
            param.requires_grad = True

    # =========================
    # 3. 优化器：只优化需要梯度的参数
    # =========================
    optimizer = optim.Adam(
        filter(lambda p: p.requires_grad, model.parameters()),
        lr=lr
    )

    # =========================
    # 4. 学习率调度器
    # =========================
    # 每 3 个 epoch 将学习率乘以 0.5
    scheduler = optim.lr_scheduler.StepLR(
        optimizer,
        step_size=3,
        gamma=0.5
    )

    # =========================
    # 5. 损失函数
    # =========================
    # NLLLoss 配合 LogSoftmax 使用
    criterion = nn.NLLLoss()

    # =========================
    # 6. 开始训练循环
    # =========================
    for epoch in range(1, epochs + 1):

        # 6.1 设置模型为训练模式
        #     启用 Dropout / BatchNorm 的训练行为
        model.train()

        total_loss = 0
        correct = 0
        total = 0

        # 6.2 遍历训练集
        for batch_idx, (data, target) in enumerate(train_loader):

            # 6.3 数据搬到设备（CPU / GPU）
            data, target = data.to(device), target.to(device)

            # 6.4 清空梯度（防止梯度累加）
            optimizer.zero_grad()

            # 6.5 前向传播
            output = model(data)

            # 6.6 计算损失
            loss = criterion(output, target)

            # 6.7 反向传播
            loss.backward()

            # 6.8 更新参数
            optimizer.step()

            # =========================
            # 7. 统计指标
            # =========================
            total_loss += loss.item()
            pred = output.argmax(dim=1)
            correct += pred.eq(target).sum().item()
            total += target.size(0)

        # 7.1 更新学习率
        scheduler.step()

        # 7.2 计算准确率
        acc = 100. * correct / total

        # 7.3 打印当前 epoch 结果
        print(f"Epoch {epoch:02d} | "
              f"Loss: {total_loss/len(train_loader):.4f} | "
              f"Acc: {acc:.2f}% | "
              f"LR: {scheduler.get_last_lr()[0]:.6f}")

# 执行训练
train_stage(stage=1, epochs=5, lr=0.001, train_all=False)   # 只训练分类器
train_stage(stage=2, epochs=15, lr=0.0001, train_all=True)  # 微调整个网络
</code></pre> 
<p>训练好之后，可以先拿原生测试数据集(EMNIST)进行大规模测试，看看准确率怎么样，测试代码如下所示：</p> 
<pre><code># ========== 1. 加载你训练好的模型（和训练时结构完全一致） ==========
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = Net(num_classes=62).to(device)
checkpoint = torch.load("emnist_62class_final.pt", map_location=device)
model.load_state_dict(checkpoint["model_state_dict"])
model.eval()
print(f" 模型加载成功，训练时准确率: {checkpoint.get('accuracy', '未知')}%")

# ========== 2. 加载EMNIST测试集（和训练时用完全一样的transform！） ==========
test_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

test_dataset = datasets.EMNIST(
    root="./data",
    split="byclass",  # 必须和训练时一致！
    train=False,
    download=False,
    transform=test_transform
)

print(f" 测试集总样本数: {len(test_dataset)}")
print(f" 标签范围: {min(test_dataset.targets.numpy())} ~ {max(test_dataset.targets.numpy())}")

# ========== 3. 全量预测 + 统计 ==========
correct_total = 0
correct_digit = 0
total_digit = 0
correct_upper = 0
total_upper = 0
correct_lower = 0
total_lower = 0

# 收集错误样本：key是(真实标签, 预测标签)，value是(图片tensor, 出现次数)
error_cases = {}

with torch.no_grad():
    for idx in range(len(test_dataset)):
        img, true_label = test_dataset[idx]
        # 增加batch维度
        img_input = img.unsqueeze(0).to(device)

        # 预测
        output = model(img_input)
        pred_label = output.argmax(dim=1).item()
        prob = torch.exp(output).max().item()

        # 统计总准确率
        if pred_label == true_label:
            correct_total += 1
        else:
            # 记录错误样本
            error_key = (true_label, pred_label)
            if error_key not in error_cases:
                error_cases[error_key] = []
            error_cases[error_key].append((img.cpu(), prob))

        # 分大类统计
        if true_label &lt; 10:  # 数字
            total_digit += 1
            if pred_label == true_label:
                correct_digit += 1
        elif true_label &lt; 36:  # 大写字母
            total_upper += 1
            if pred_label == true_label:
                correct_upper += 1
        else:  # 小写字母
            total_lower += 1
            if pred_label == true_label:
                correct_lower += 1

# ========== 4. 输出结果 ==========
total_acc = 100 * correct_total / len(test_dataset)
digit_acc = 100 * correct_digit / total_digit if total_digit &gt; 0 else 0
upper_acc = 100 * correct_upper / total_upper if total_upper &gt; 0 else 0
lower_acc = 100 * correct_lower / total_lower if total_lower &gt; 0 else 0

print("\n" + "="*60)
print(f" 全量测试结果（共{len(test_dataset)}张图）:")
print(f"   总准确率: {total_acc:.2f}%")
print(f"   数字(0-9)准确率: {digit_acc:.2f}% ({correct_digit}/{total_digit})")
print(f"   大写字母(A-Z)准确率: {upper_acc:.2f}% ({correct_upper}/{total_upper})")
print(f"   小写字母(a-z)准确率: {lower_acc:.2f}% ({correct_lower}/{total_lower})")
print("="*60)

# ========== 5. 可视化最常见的错误 ==========
if len(error_cases) &gt; 0:
    print("\n 最常见的5种错误:")
    # 按错误次数排序
    sorted_errors = sorted(error_cases.items(), key=lambda x: len(x[1]), reverse=True)[:5]

    for (true_lbl, pred_lbl), cases in sorted_errors:
        # 字符映射
        def label_to_char(lbl):
            if lbl &lt; 10: return str(lbl)
            elif lbl &lt; 36: return chr(ord('A') + lbl -10)
            else: return chr(ord('a') + lbl -36)

        true_char = label_to_char(true_lbl)
        pred_char = label_to_char(pred_lbl)
        count = len(cases)
        print(f"   {true_char}(标签{true_lbl}) → {pred_char}(标签{pred_lbl}): {count}次")

        # 可视化前3个错误样本
        plt.figure(figsize=(10, 3))
        for i in range(min(3, len(cases))):
            img_tensor, prob = cases[i]
            plt.subplot(1, 3, i+1)
            plt.imshow(img_tensor.squeeze(), cmap="gray")
            plt.title(f"真:{true_char} 预:{pred_char}\n置信度:{prob:.3f}")
            plt.axis("off")
        plt.suptitle(f"错误类型: {true_char}→{pred_char}（共{count}次）")
        plt.show()
else:
    print("\n 没有错误样本！模型100%准确（几乎不可能，说明测试逻辑有问题）")
</code></pre> 
<p>输出如下信息：</p> 
<p> 模型加载成功，训练时准确率: 84.91269998194682%<br>  测试集总样本数: 116323<br>  标签范围: 0 ~ 61</p> 
<hr> 
<p> 全量测试结果（共116323张图）:<br> 总准确率: 84.91%<br> 数字(0-9)准确率: 95.64% (55390/57918)<br> 大写字母(A-Z)准确率: 82.09% (25733/31346)<br> 小写字母(a-z)准确率: 65.23% (17650/27059)</p> 
<p>这个准确率说明模型<strong>没有过拟合、没有崩溃、学到了有效特征</strong>，但是为什么达不到90%呢？其中有几个原因：</p> 
<ul> 
 <li>类别极度不平衡：EMNIST ByClass 中数字样本多，并且相似字符会互相干扰</li> 
 <li>迁移学习上限：因为是从 <strong>MNIST（10 类）</strong>​ 迁移到 <strong>EMNIST（62 类）</strong>，所以预训练模型的卷积层是为“数字边缘”设计的，不是为“字母曲线”</li> 
</ul> 
<p>但可以看到其中对数字的识别最准，小写字母不太好分辨</p> 
<h3>模型测试</h3> 
<p>遇到一个最隐蔽的坑 —— 图片方向。我写了一个大写 <strong>A</strong>，模型说是 <strong>F / t / r</strong>，直到我把图片<strong>旋转 90°</strong>，它才认出来。排查出原因如下：</p> 
<ul> 
 <li>EMNIST 来自 NIST 扫描表单</li> 
 <li>原始数据方向并不统一</li> 
 <li>我“正着写”，模型“横着看”</li> 
 <li>典型的<strong>训练-测试分布不一致</strong>问题</li> 
</ul> 
<p>测试模型的过程同上，也是图片预处理和模型输入预处理，直接看结果吧</p> 
<p>先鼠标画一张“A"图片作为模型输入的图片，做了90°旋转</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172445447-1986623963.png"></p> 
<p>下面是模型的TOP-5预测结果：</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172454298-2118806380.png"></p> 
<p>还有62个分类的热力图：</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172502450-2019645721.png"></p> 
<p>然后再测试数字“3”，也做了旋转处理，向右旋转90°，结果如下所示：</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172509256-1540238128.png"></p> 
<p>最后测试小写字母“j”，也是调整图片，旋转、平移后，结果如下所示：</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260625172515923-1351262049.png"></p> 
<p>整体来说，迁移学习训练出来的模型有点差强人意，实际使用中，我还得去配合模型的约束与参数，不然模型效果很不理想，有点本末倒置了，应该就是<strong>训练-测试分布不一致</strong>导致的问题吧？</p> 
<p>我多次调整训练过程，发现针对<code>EMNIST</code>测试集的准确率都在及格范围内，但是实际使用我的鼠标写的字符图片就需要不断调整才有可能识别正确，有的图片我怎么调整都识别不对，就很心累。</p> 
<h2>总结</h2> 
<p>回顾整个学习调试历程，我经历了三个阶段的跃迁：</p> 
<ol> 
 <li><strong>调包阶段</strong>：知道怎么跑，不知道为什么。</li> 
 <li><strong>解剖阶段</strong>：想知道每一层在干什么，于是学会了可视化和原理。</li> 
 <li><strong>调试阶段</strong>：不满足于现有模型，开始尝试微调模型，以满足个性化需求。</li> 
</ol> 
<p>如果你也刚开始学，不要急着跑更复杂的模型（ResNet、Transformer）。<strong>先把 MNIST 这个“麻雀”解剖明白了.</strong> ​当你能对着一张 28×28 的图，说出它经过每一层后尺寸怎么变、为什么变的时候，就算入门了。</p> 
<p>而从 MNIST 迁移到 EMNIST 的这一步，过程中遇到的问题和挑战是很伤人脑筋的，你会发现，一个模型只适用于它学习过的内容，对于额外的内容它也是难以把握。训练的过程是让模型掌握新知识的过程，但是新知识如何融合进已有的内容就很值得思考了。</p> 
<p>最后，如果想要代码的话，可以联系我，我会给我实验过程的jupyter文档。如果文章有什么问题，或者有什么指教都可以联系我。</p> 
<p>微信公众号：软趴趴的工程师(一个乐于助人的工程师)</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/1358160/202606/1358160-20260615110547800-1086888969.png"></p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:48 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-105.html</guid>
</item>
<item>
    <title>Agent Mail 产品介绍与 Trae Solo Agent 实测</title>
    <link>http://www.51keeplearning.com/post-104.html</link>
    <description><![CDATA[<h2>Agent Mail 产品介绍与 Trae Solo Agent 实测</h2> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101411461-1761370480.png" target="_blank" rel="nofollow"><img style="border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101411461-1761370480.png" class="aligncenter"></a></p> 
<h2>一、产品基础信息</h2> 
<p><font>Agent Mail（Agently Mail）是 QQ 邮箱团队面向 AI Agent 推出的专属邮箱服务，官网：</font><a href="https://link.wtturl.cn/?target=https%3A%2F%2Fagent.qq.com%2F&amp;scene=im&amp;aid=497858&amp;lang=zh" target="_blank" rel="nofollow"><font>https://agent.qq.com/</font></a><font>，官方帮助文档：</font><a href="https://link.wtturl.cn/?target=https%3A%2F%2Fhelp.agent.qq.com%2Fdetail%2F0%2F1092&amp;scene=im&amp;aid=497858&amp;lang=zh" target="_blank" rel="nofollow"><font>https://help.agent.qq.com/detail/0/1092</font></a><font>。 </font></p> 
<p><font>核心特性：</font></p> 
<ol> 
 <li><font>数据隔离：独立生成专属邮箱地址，和个人 QQ / 微信邮箱完全分隔，不会读取、操作私人邮件，隐私安全性更高； </font></li> 
 <li><font>原生适配：无需手动安装复杂工具，仅发送指定指令即可自动完成 <a href="https://github.com/resources/articles/what-is-a-cli" target="_blank" rel="nofollow">CLI</a> 与配套技能部署； </font></li> 
 <li><font>操作便捷：接入后依靠自然语言指令完成发信、查收、检索、附件收发等全流程邮件操作，还可搭建发票处理、邮件自动归档等自动化工作流； </font></li> 
 <li><font>兼容范围：支持海内外主流 AI Agent，<a href="https://docs.trae.cn/ide_solo-coder" target="_blank" rel="nofollow">Trae Solo Agent</a> 可正常接入使用。 </font></li> 
</ol> 
<p><font>完整接入标准流程：</font></p> 
<ol> 
 <li><font>在 Agent 对话框发送配置指令：请阅读</font><a href="https://link.wtturl.cn/?target=https%3A%2F%2Fagent.qq.com%2Fdoc%2Fcli-setup.md&amp;scene=im&amp;aid=497858&amp;lang=zh" target="_blank" rel="nofollow"><font>https://agent.qq.com/doc/cli-setup.md</font></a><font> 文档，按照步骤为我安装并配置 Agent Mail CLI； </font></li> 
 <li><font>打开 Agent 返回的授权链接，微信扫码完成授权，系统自动生成独立邮箱； </font></li> 
 <li><font>页面提示登录成功后返回对话窗口，若未自动识别授权状态，手动发送 “已完成授权” 即可完成接入，收到提示「邮箱地址 xxx 已授权成功，可以用它来收发邮件了」代表开通完毕。</font></li> 
</ol> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101413045-2100548776.png" target="_blank" rel="nofollow"><font><img style="border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101413614-1179470105.png" class="aligncenter"></font></a></p> 
<h2>二、Trae Solo Agent 实操测试流程</h2> 
<h5><font>1. 首次发信操作</font></h5> 
<p><font>在 Trae Solo Agent 对话内输入指令：</font></p> 
<blockquote> 
 <p><font><code>/ agently-mail</code> 帮我给kmdxdk1@hotmail.com 发一封邮件，内容是 "AI 变革" </font></p> 
</blockquote> 
<p><font>操作触发浏览器跳转授权页面，完成扫码再次授权后，邮件发送流程执行成功，前往目标收件邮箱核对，成功收到本次主题 / 内容为 “AI 变革” 的邮件。</font></p> 
<h5><font>2. 授权与稳定性表现</font></h5> 
<ol> 
 <li><font>初次完整使用需完成一次微信授权，授权链路依赖浏览器跳转，偶发需二次扫码确认的情况，整体稳定性仍有待长期评测； </font></li> 
 <li><font>二次及后续使用无需重复授权确认，可直接通过自然语言发起收发邮件操作。</font></li> 
</ol> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101414411-920010792.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101414411-920010792.png" class="aligncenter"></font></a></p> 
<blockquote> 
 <p><font>/ agently-mail` 帮我给kmdxdk1@hotmail.com 发一封邮件，内容是"AI变革"</font></p> 
</blockquote> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101415398-107512048.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101415398-107512048.png" class="aligncenter"></font></a></p> 
<p><font>再次授权</font></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101416418-668142025.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101416897-1073892921.png" class="aligncenter"></font></a></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101417412-731620561.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101417412-731620561.png" class="aligncenter"></font></a></p> 
<p><font>在弹出浏览器，再次授权后，可以发送邮件成功</font></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101418413-403899851.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101418413-403899851.png" class="aligncenter"></font></a></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101419284-93875396.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101419762-1254735482.png" class="aligncenter"></font></a></p> 
<p><font>查看邮箱确实 收到了</font></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101420268-1237249189.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101420268-1237249189.png" class="aligncenter"></font></a></p> 
<p><font>整体过程需要一次授权，稳定性待评测。</font></p> 
<p><font>第二次试用不需要确认</font></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101421160-646527407.png" target="_blank" rel="nofollow"><font><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101421160-646527407.png" class="aligncenter"></font></a></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101422031-572664612.png" target="_blank" rel="nofollow"><font><img style="border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626101422031-572664612.png" class="aligncenter"></font></a></p> 
<h2>三、管理与常见问题说明</h2> 
<ol> 
 <li><font>邮箱后台管理：登录</font><a href="https://link.wtturl.cn/?target=https%3A%2F%2Fagent.qq.com&amp;scene=im&amp;aid=497858&amp;lang=zh" target="_blank" rel="nofollow"><font>agent.qq.com</font></a><font>即可查看、管理专属 Agent 邮箱，支持修改发件昵称与邮箱地址，单个邮箱仅可修改一次，修改后旧地址直接失效； </font></li> 
 <li><font>授权失败处理：完整复制授权链接至浏览器打开，若链接过期可发送指令「请帮我生成新的 Agent Mail 的授权链接」重新获取； </font></li> 
 <li><font>已知问题：WorkBuddy 存在反复授权异常问题，官方正在优化修复；使用过程中遇到功能故障、需求建议，可通过官方反馈渠道提交。</font></li> 
</ol> 
<h2>四、通用自然语言使用指令示例</h2> 
<p><br></p> 
<table border="0" cellpadding="0"> 
 <tbody> 
  <tr> 
   <td><p><b><font>使用场景</font></b></p></td> 
   <td><p><b><font>自然语言指令参考</font></b></p></td> 
  </tr> 
  <tr> 
   <td><p><font>发送邮件</font></p></td> 
   <td><p><font>帮我给 xx@shturl. 发一封邮件，内容是……</font></p></td> 
  </tr> 
  <tr> 
   <td><p><font>查看收件</font></p></td> 
   <td><p><font>看看我最近收到了哪些邮件</font></p></td> 
  </tr> 
  <tr> 
   <td><p><font>检索历史邮件</font></p></td> 
   <td><p><font>找一下上周关于项目进度的那封邮件</font></p></td> 
  </tr> 
  <tr> 
   <td><p><font>邮件回复</font></p></td> 
   <td><p><font>回复刚才那封邮件，告诉对方我已收到</font></p></td> 
  </tr> 
  <tr> 
   <td><p><font>附件收发</font></p></td> 
   <td><p><font>发邮件给 xxx，并把这份文件作为附件；把主题为 xxx 邮件的附件下载到本地</font></p></td> 
  </tr> 
  <tr> 
   <td><p><font>删除邮件</font></p></td> 
   <td><p><font>删除主题为 xxx 的邮件</font></p></td> 
  </tr> 
 </tbody> 
</table> 
<h2>适配 AI Agent 时代，打通工具自主调用通路</h2> 
<p><font>传统 GUI 面向人类视觉交互，AI 无法自主识别按钮、页面层级；CLI 以纯文本指令、标准化参数作为交互载体，是大模型天然适配的 “母语”。以 Agently Mail 为例：QQ 邮箱传统网页端、客户端均为 GUI，AI 无法自主操作；通过封装 CLI 工具，仅需一句配置指令，Agent 即可自动完成安装、授权、收发邮件全流程，自然语言可直接转译为邮件操作命令，实现 AI 自主处理邮件工作流（自动对账、发票归集、批量发信）。</font></p> 
<ol> 
 <li><font><strong>降低 AI 调用成本</strong>：CLI 自带<code>--help</code>文档，无需完整 API 文档灌入模型，大幅减少 Token 消耗；相比调用 REST API 需编写鉴权、请求代码，CLI 一行命令即可执行，降低 Agent 工具集成门槛。 </font></li> 
 <li><font><strong>标准化通用交互层</strong>：CLI 统一 “命令 + 参数 + 返回文本” 结构，国内外主流 Agent（Trae Solo、Claude、Kimi 等）可无差别接入，解决传统应用多端适配、多 Agent 兼容难题。</font></li> 
</ol> 
<p><font><strong>战略价值</strong>：CLI 化是传统软件接入 AI Agent 生态的标准化改造路径，解决 GUI 无法被机器自主调用的底层缺陷，实现工具从 “仅供人使用” 向 “人机协同、AI 自主自动化” 转型，是企业数字化、AI 办公落地的基础设施改造；</font></p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:48 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-104.html</guid>
</item>
<item>
    <title>MCP 到底是什么？为什么 Agent 都想接上它</title>
    <link>http://www.51keeplearning.com/post-103.html</link>
    <description><![CDATA[<p>当我们聊起 Agent，或者阅读相关内容时，经常会看到一个词：MCP。它看起来像一个偏底层的技术名词，我们可以先来简单地记住这样一句话：<strong>MCP 是一套让 Agent 连接外部资料和工具的通用方式。</strong></p> 
<p>为什么 Agent 需要 MCP？因为我们要让 Agent 真正干活，第一步往往是得让它看见任务现场。要排查代码问题，它得先看到代码；要分析一次报错，它得先看到日志；要总结项目进展，它得看到文档、Issue、提交记录和任务状态。只靠聊天框里我们输入的几句文字描述，Agent 很容易给出泛泛建议。</p> 
<p>因为 Agent 真能干活要的信息，大多都在模型外面。没有合适的连接方式，Agent 就只能根据你贴给它的内容猜着回答。你没贴日志，它就看不到日志；你没给仓库，它也没法自己打开文件。</p> 
<p>所以，MCP 解决的是一个很朴素的问题：<strong>Agent 怎么接上外部世界。</strong></p> 
<h2>Agent 是一个新同事</h2> 
<p>我们可以把 Agent 想象成一个新来的同事。然后这个同事也很聪明，理解能力不错，还能帮你分析问题。但因为它刚进公司，什么系统权限都没有，既看不到文档，刚领取的电脑里也没有项目代码。你要让它直接来处理问题，它就只能根据你的口头描述来猜该怎么办。</p> 
<p>这时候，你给它开通了一些工作入口：可以查看代码仓库，查询日志系统、数据库中的部分字段，读取任务系统中的 Issue，在必要时还能运行测试。就这样，它能接触到的资料越完整，能调用的工具越清楚，处理任务的能力就越强，也就越接近现实中来搬砖的同事。</p> 
<p>MCP 就是给这个“新同事”准备的一套标准工作入口。它告诉 Agent：哪些资料可以看，哪些工具可以用，每个工具怎么调用，调用后会返回什么结果。</p> 
<p>这就是 MCP 最核心的作用。它不负责替 Agent 思考（有大模型），也不直接改变模型能力。它只负责把外部资料和工具，用一种更统一的方式接到 Agent 面前。</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/3796273/202606/3796273-20260626103523251-1923142628.png"></p> 
<h2>MCP 是一个“统一插座”</h2> 
<p>换一个角度看，你也可以当 MCP 是个“统一插座”。外部工具像各种设备，Agent 像一台需要接设备的电脑。过去，Agent 要想接一个工具，往往要单独做一次适配。接 GitHub，要写一套适配逻辑；接数据库，要写一套适配逻辑；接本地文件系统，还要再写一套。工具越多，连接方式就越碎，我们要做的接入工作就越重复。</p> 
<p>MCP 想做的事情，是把这类连接方式尽量标准化。工具提供方按照 MCP 约定好的方式，把自己的能力暴露出来；AI 应用也可以按照 MCP 的方式去连接这些能力。这样一来，一个工具就比较容易被不同 Agent 使用，一个 Agent 也更容易接入不同工具。</p> 
<p>像上面提到的 GitHub 例子，它就可以提供代码仓库、Issue、PR 相关能力；数据库可以提供查询能力；文件系统可以提供读写文件能力；浏览器工具可以提供网页访问能力。这些能力用 MCP 的方式接出来的话，Agent 就更容易知道自己能用什么、应该怎么用。</p> 
<h2>Agent 接上 MCP 后能做什么</h2> 
<p>Agent 接上 MCP 后，最重要的变化是两件事：<strong>更容易拿到上下文，也更容易调用工具。</strong></p> 
<p>上下文是 Agent 做判断时需要看的资料，比如代码文件、产品文档、错误日志、数据库记录、网页内容、Issue 讨论。没有上下文，Agent 很容易说一些正确但没用的话，因为它不知道现场到底发生了什么。</p> 
<p>工具是 Agent 可以调用的动作，比如搜索文件、查询数据库、创建 Issue、运行测试、调用接口、生成报告。工具让 Agent 不只停留在“给建议”，还能继续完成任务里的下一步。</p> 
<p>这就是 MCP 对 Agent 的帮助：它让 Agent 更容易走进任务现场，也更容易沿着任务继续往下做。</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/3796273/202606/3796273-20260626103757720-1705268574.png"></p> 
<h2>为什么 Agent 都想接上它</h2> 
<p>Agent 想处理真实任务，就绕不开外部系统。写代码要接仓库、文件、测试、CI 和 Issue；做数据分析要接数据库、表格、报表平台和业务文档；做办公自动化要接邮件、日历、知识库和工单系统。只靠聊天窗口，Agent 很难持续推进这些任务。</p> 
<p>MCP 的吸引力就在这里。对 Agent 产品来说，支持 MCP 意味着它更容易接入一批外部能力。对工具提供方来说，做成 MCP Server 之后，自己的工具更容易被不同 AI 应用使用。对普通用户来说，最直观的感受是：Agent 能连接的东西变多了，能处理的任务也更贴近日常工作。</p> 
<p>这也是为什么 MCP 经常和 Agent 一起出现。Agent 的能力不只取决于模型本身，也取决于它能看到什么、能调用什么、能不能在任务中继续推进。很多时候，AI 回答不准，并不是因为它不会分析，而是因为它根本没看到关键资料。</p> 
<h2>MCP 里面有几个角色</h2> 
<p>如果稍微往技术结构上看，MCP 里通常会提到三个角色：Host、Client、Server。这是它们的分工：</p> 
<ul> 
 <li> <p>Host 是你正在使用的 AI 应用，比如桌面助手、IDE 插件或 Agent 平台；</p> </li> 
 <li> <p>Client 是 Host 里负责连接 MCP 的部分，可以理解成里面的连接器；</p> </li> 
 <li> <p>Server 是外部能力的提供者，比如 GitHub MCP Server、数据库 MCP Server、文件系统 MCP Server。</p> </li> 
</ul> 
<p>用前面的插座比喻来说，Host 是工作台，Client 是插口，Server 是插上来的外部设备。Agent 需要某个能力时，就通过这条路径去访问外部资料或调用工具。</p> 
<p>我们不一定一开始就记住这些名字，核心是 MCP 让 AI 应用和外部工具之间，有了一套更统一的沟通方式。</p> 
<h2>如何在开发中用上 MCP</h2> 
<p>放到开发里看，MCP 的一个直接作用，是把已有工具包装成 Agent 能调用的能力。</p> 
<p>举个例子，公司内部有一个后台系统，可以查询用户订单、服务日志、任务状态和错误记录。过去这些查询能力主要是给人用的，Agent 很难直接用上。但我们可以写一个 MCP Server，把其中安全、明确、可控的能力暴露出来。这样，即使 Agent 不完全理解整个后台系统，只要知道这里有几个可用工具：查询订单状态、查看任务日志、创建排查工单、生成故障摘要，就可以干更多的事情了。</p> 
<p>这样做的好处，就是内部工具可以被更多 AI 应用复用。团队不用每接一个 Agent 产品，就重新适配一次自己的系统。对企业来说，这也是 MCP 被关注的原因之一：它让内部知识、业务系统和 AI 助手之间的连接更容易标准化。</p> 
<p>不过，这里要注意一个安全问题：Agent 接入工具能力的同时，其实也在获得部分系统权限。如果 Agent 可以读文件，就要限制它能读哪些目录；如果它可以查数据库，就要限制它能查哪些表和字段；如果它可以执行命令，就要有白名单、沙箱和日志；如果它可以修改线上系统，就应该加入人工确认和回滚机制。</p> 
<p>MCP 让连接更方便，但安全边界仍然要由工程系统来保证。</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/3796273/202606/3796273-20260626103812195-34886156.png"></p> 
<h2>MCP 容易被误解的地方</h2> 
<p>第一个误解是：接上 MCP，Agent 就会自动变强。实际情况没有这么简单。MCP 解决的是连接问题，它让资料和工具更容易进入 Agent 的工作流程。但 Agent 能不能用好这些工具，还取决于模型能力、工具描述、任务拆解、错误处理和权限设置。工具太多、说明不清、返回结果混乱，都会让 Agent 做出错误选择。</p> 
<p>第二个误解是：MCP 只适合写代码。Coding Agent 确实是最容易理解的场景，因为写代码天然需要接文件、仓库、终端、测试和文档。但 MCP 的适用范围不止开发工具。客服 Agent 可以接知识库和工单系统，数据 Agent 可以接数据库和报表平台，研究 Agent 可以接论文库和网页搜索，企业助手可以接内部文档和业务 API。只要任务需要外部资料和工具，MCP 就有可能派上用场。</p> 
<p>第三个误解是：有了 MCP，就不用 API 了。其实很多 MCP Server 背后仍然是在调用 API、读取数据库或访问文件系统。MCP 更像是给 AI 应用准备的一层连接包装，让 Agent 更容易发现、理解和调用这些能力。</p> 
<h2>小结</h2> 
<p>MCP 可以理解成 Agent 时代的一种通用连接方式。它解决的问题很具体：AI 应用怎样更方便地接入外部资料和工具。</p> 
<p>对 Agent 来说，MCP 让它更容易读取文档、访问代码、查询数据、调用工具，进入更真实的任务流程。对开发者来说，MCP 让工具能力更容易被复用，减少重复适配的工作。</p> 
<p>但 MCP 不是万能按钮。它不会自动让 Agent 变聪明，也不会自动保证每次操作都安全。真正可靠的 Agent 系统，还需要清晰的工具设计、合理的权限控制、必要的人工确认和完整的操作记录。</p> 
<p>所以，可以用一句话理解 MCP：Agent 想从聊天窗口走向真实工作流，就需要一套稳定、通用、可治理的连接方式。MCP 做的，就是把这条连接路径铺出来。</p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:47 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-103.html</guid>
</item>
<item>
    <title>四周实现非母语教学APP</title>
    <link>http://www.51keeplearning.com/post-102.html</link>
    <description><![CDATA[<p><font><b>极致的极简主义</b>和无干扰设计（Zero-Distortion Design）正是目前少儿启蒙产品最稀缺的特性。现在的App充斥着各种金币、动画和复杂UI，反而分散了孩子的注意力。</font></p> 
<p><font>每关15个短句30个插画，全部都是根据我的提示词AI生成，教材是让ChatGPT蒸馏了Reading Eggs、Raz Kids、Oxford Discover、Oxford Phonics World、Starfall、Cambridge Young Learners这些，并且根据非母语孩子的学习路径进行了微调。</font></p> 
<p><font><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152938001-1186929464.png" target="_blank" rel="nofollow"><img style="border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152938001-1186929464.png" class="aligncenter"></a></font></p> 
<p><font>把这个极简的构想落地成一款真正的App，我们需要从<b>内容生成（ChatGPT蒸馏）</b>、<b>视觉生成（AI插画）</b>、<b>技术架构（极简前端）<b>以及</b>核心交互机制</b>四个维度来详细解读如何实现。</font></p> 
<h2>一、 内容层：ChatGPT 蒸馏与微调（教研核心）</h2> 
<p><font>你提到的这几个教材（RAZ、Oxford、Reading Eggs等）各有侧重（有侧重拼读的，有侧重分级阅读的）。通过 ChatGPT 蒸馏并结合非母语（EFL）路径，你需要建立一个<b>结构化的 Prompt 管道</b>。</font></p> 
<h5><font>1. 蒸馏的逻辑架构</font></h5> 
<p><font>非母语孩子的路径通常是：<b>听音辨意（词汇） –&gt; 核心句型（日常交流）&nbsp; –&gt; 拼读规律（Phonics） –&gt;场景高频词（Sight Words）</b>。</font></p> 
<table border="0" cellpadding="0"> 
 <tbody> 
  <tr> 
   <td><p><b><font>教材名称</font></b></p></td> 
   <td><p><b><font>核心长处（取什么）</font></b></p></td> 
   <td><p><b><font>蒸馏策略</font></b></p></td> 
  </tr> 
  <tr> 
   <td><p><b><font>Starfall</font></b></p></td> 
   <td><p><font>26个字母基础音、极其简单的初级视觉词</font></p></td> 
   <td><p><font>提炼为 <b>Level 1</b> 的骨架，解决最初的“见字认音”或最基础的日常单字。</font></p></td> 
  </tr> 
  <tr> 
   <td><p><b><font>Oxford Phonics World</font></b></p></td> 
   <td><p><font>极其系统和地道的拼读规律组合（如 fat cat, red hen）</font></p></td> 
   <td><p><font>提取其中的<b>拼读核心词（CVC词汇）</b>，用于设计押韵、好读、具象化的短句。</font></p></td> 
  </tr> 
  <tr> 
   <td><p><b><font>Reading Eggs</font></b></p></td> 
   <td><p><font>游戏化高频词（Sight Words）的学习节奏</font></p></td> 
   <td><p><font>提取其<b>高频词库</b>（如 <i>the, is, on, under</i>），将其穿插到短句中。</font></p></td> 
  </tr> 
  <tr> 
   <td><p><b><font>Raz Kids</font></b></p></td> 
   <td><p><font>严密的分级阅读（从 aa 到 Z）和高频句型重复</font></p></td> 
   <td><p><font>学习其**“句型滚动”**的逻辑。比如这一关是 <i>I see a...</i>，下一关就是 <i>I see a big...</i>。</font></p></td> 
  </tr> 
  <tr> 
   <td><p><b><font>Oxford Discover</font></b></p></td> 
   <td><p><font>探究式的大视野词汇（大自然、科学、社会）</font></p></td> 
   <td><p><font>在后续较高关卡中，提取其<b>具象的场景词汇</b>，避免日常对话过于单一。</font></p></td> 
  </tr> 
  <tr> 
   <td><p><b><font>Cambridge Young Learners</font></b></p></td> 
   <td><p><font>剑桥少儿英语（Starters/Movers）的权威测评词汇</font></p></td> 
   <td><p><font>用来做<b>质量把关（QA）</b>。确保蒸馏出的词汇完美覆盖剑桥少儿英语核心词纲。</font></p></td> 
  </tr> 
 </tbody> 
</table> 
<h5><font>2. ChatGPT 蒸馏提示词（Prompt）示例</font></h5> 
<p><font>你可以分批次让 ChatGPT 生成关卡内容。为了方便后续开发，<b>必须要求 AI 输出结构化的 JSON 格式</b>：</font></p> 
<blockquote> 
 <p><font><b>System Prompt:</b> &gt; 你是一位精通少儿英语启蒙（EFL路径）的资深教研专家。请结合 Reading Eggs 的趣味性、RAZ 的分级严谨性、以及 Oxford Phonics World 的拼读规律，为4岁非母语孩子设计关卡内容。</font></p> 
 <p><b><font>User Prompt:</font></b></p> 
 <p><font>请生成第 1 关的内容。主题为“常见小动物与基础动作”。</font></p> 
 <p><font>核心要求：</font></p> 
 <ol> 
  <li><p><font>包含 15 个极其简短的英语短句（控制在 2-4 个词）。</font></p></li> 
  <li><p><font>每句对应 2 张插画的生成提示词（一张<b>正确图</b>，一张<b>干扰图</b>）。干扰图要具备相关性但有明显区别（例如：正确是"A big cat"，干扰是"A small dog"，而不是风马牛不相及的东西）。</font></p></li> 
  <li><p><font>请以 JSON 格式输出，字段包括：<code>id</code> (1-15), <code>sentence</code> (英语短句), <code>correct_image_prompt</code> (正确图AI提示词), <code>distractor_image_prompt</code> (干扰图AI提示词)。</font></p></li> 
 </ol> 
</blockquote> 
<h5><font>教研 Prompt（提示词）工业级模板</font></h5> 
<blockquote> 
 <p>## Role:</p> 
 <p>你是全球顶尖的少儿英语教研总监，精通 EFL（英语非母语）儿童认知发展心理学。</p> 
 <p>## Task:</p> 
 <p>请融合 Starfall 的拼读启蒙、Raz-Kids (Level aa-A) 的句型复现、Oxford Phonics World 的韵律感，为 3-5 岁零基础中国孩子设计第 [X] 关的内容。</p> 
 <p>## 关卡微调黄金法则：</p> 
 <ol> 
  <li>严控字数： 纯粹的听力辨意。本关每个短句严格控制在 2-4 个单词。</li> 
  <li>认知对齐： 句子描绘的画面必须能用一张图完美表达。严禁出现“抽象概念”（如 "He likes English" 无法具象化）。</li> 
  <li>干扰项设计（核心）： 干扰图绝对不能风马牛不相及（例如正确是“红苹果”，干扰绝对不能是“一辆卡车”）。 
   <ul> 
    <li><i>初级干扰：</i> 类别相同，物种不同（例：A cat vs A dog）。</li> 
    <li><i>高级干扰：</i> 核心词相同，属性不同（例：A big pig vs A small pig）。</li> 
   </ul></li> 
 </ol> 
 <p>格式要求 (必须严格输出纯 JSON 数组，严禁任何解释性文字)：</p> 
 <p>JSON</p> 
 <p>[</p> 
 <p> {</p> 
 <p> "id": 1,</p> 
 <p> "sentence": "当前关卡的英文短句",</p> 
 <p> "focus_phonics_or_word": "本题考察的核心词/发音（如 short a / big）",</p> 
 <p> "correct_image_prompt": "给Midjourney的正确图英文提示词。要求：3D vector art, cute clay style, bold colors, white background, isolated, children app style",</p> 
 <p> "distractor_image_prompt": "给Midjourney的干扰图英文提示词。要求：风格、构图与正确图高度一致，但核心特征有明显区分"</p> 
 <p> }</p> 
 <p>]</p> 
</blockquote> 
<p><br></p> 
<h5><font>3. 生成的 JSON 数据示例</font></h5> 
<p><font>JSON</font></p> 
<pre><code><font>[ { "id": 1, "sentence": "A happy dog.", "correct_image_prompt": "A cute happy puppy wagging its tail, simple vector illustration for children, white background, bright colors --ar 4:3", "distractor_image_prompt": "A sleepy cat sleeping on a rug, simple vector illustration for children, white background, bright colors --ar 4:3" } ] </font></code></pre> 
<h4><font><br></font></h4> 
<p><strong>claude 4.6</strong></p> 
<p><br></p> 
<p>[<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 1,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A big cat",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one big orange cat sitting, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one small orange cat sitting, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 2,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "The dog runs",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, a brown dog running fast with legs stretched out, motion lines, bright colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, a brown dog sleeping curled up with closed eyes, soft colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 3,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A red bird",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one small red bird perched on a branch, simple shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one small blue bird perched on a branch, simple shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 4,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "The bird flies",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, a small bird flying with wings spread wide in the sky, bright colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, a small bird sitting still on a branch with wings folded, bright colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 5,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A big fish",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one big blue fish swimming, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one small blue fish swimming, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 6,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "The fish swims",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, a blue fish swimming underwater with small bubbles, bright colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, a green frog swimming underwater with small bubbles, bright colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 7,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A green frog",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one green frog sitting on a lily pad, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one brown frog sitting on a lily pad, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 8,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "The frog jumps",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, a green frog jumping high in the air with legs tucked, motion lines, bright colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, a green frog sleeping on a lily pad with closed eyes, soft colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 9,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A white rabbit",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one white rabbit with long ears sitting upright, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one brown rabbit with long ears sitting upright, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 10,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "The rabbit runs",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, a white rabbit running fast with ears flying back, motion lines, bright colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, a white rabbit sitting still and eating a carrot, bright colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 11,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A yellow duck",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one yellow duck standing, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one white duck standing, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 12,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "The duck swims",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, a yellow duck swimming on a blue pond with small ripples, bright colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, a yellow duck walking on green grass, bright colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 13,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A black cat",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one black cat standing, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one white cat standing, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 14,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "The bear sleeps",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, a brown bear sleeping curled up with closed eyes, soft cozy colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, a brown bear walking on all fours, bright colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; },<br>&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp; "id": 15,<br>&nbsp;&nbsp;&nbsp;&nbsp; "sentence": "A small bear",<br>&nbsp;&nbsp;&nbsp;&nbsp; "correct_image_prompt": "Children's book flat illustration, one small brown bear cub standing, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style",<br>&nbsp;&nbsp;&nbsp;&nbsp; "distractor_image_prompt": "Children's book flat illustration, one big brown bear standing, simple rounded shapes, bright cheerful colors, plain white background, no text, friendly cartoon style"<br>&nbsp;&nbsp; }<br> ]</p> 
<p>AIGC第15条2张图</p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152939503-976450264.png" target="_blank" rel="nofollow"><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152939503-976450264.png" class="aligncenter"></a></p> 
<p>与</p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152940930-1404118512.png" target="_blank" rel="nofollow"><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152940930-1404118512.png" class="aligncenter"></a></p> 
<h2>二、 视觉层：AI 插画生成与风格统一</h2> 
<p><font>因为每关需要 30 张图（15 正确 + 15 干扰），AI 生成最大的痛点是<b>风格不统一</b>。如果一张是写实风，一张是简笔画，会严重破坏极简产品的精致感。</font></p> 
<h5><font>1. 风格锁定（Style Consistency）</font></h5> 
<p><font>推荐使用 <b>Midjourney (v6)</b> 或 <b>Flux.1</b>。在提示词尾部必须加入固定的“风格锁”。</font></p> 
<ul> 
 <li><p><font><b>推荐画风后缀：</b> <code>... cute cartoon illustration for kids app, flat design, vector style, bold lines, bright pastel colors, isolated on solid white background --ar 4:3</code></font></p></li> 
 <li><p><font><b>避免污染：</b> 一定要强调 <code>solid white background</code>（纯白背景），这样在 App 里渲染时不需要复杂的抠图，直接贴上就能和极简的白色背景融为一体。</font></p></li> 
</ul> 
<h5><font>2. 自动化批量生成（进阶）</font></h5> 
<p><font>如果不想手动一张张在 Discord 里敲，可以利用 Midjourney API（如第三方 API 转发服务）或 ComfyUI（结合 Flux）跑脚本，读取第一步生成的 JSON，自动批量产出图片并命名为 <code>level1_01_correct.png</code>。</font></p> 
<p><a href="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152942142-2119736868.png" target="_blank" rel="nofollow"><img style="margin: 0; border: 0 currentColor; border-image: none; display: inline; background-image: none" src="https://img2024.cnblogs.com/blog/15172/202606/15172-20260626152942799-1168155256.png" class="aligncenter"></a></p> 
<h2>三、 产品层：零干扰交互设计（UI/UX）</h2> 
<p><font>既然要“删除一切非必要按钮”，你的界面上<b>不应该出现</b>：“下一关”按钮、分数板、进度条数字、星星奖励、甚至连返回键和设置键都可以隐藏到长按触发的“家长控制区”。</font></p> 
<h5><font>1. 核心交互流程</font></h5> 
<ol> 
 <li><p><font><b>进入关卡：</b> 屏幕一片空白（纯白），0.5秒后，自动播放短句音频：<i>"A happy dog."</i></font></p></li> 
 <li><p><font><b>图片呈现：</b> 音频播放的同时（或播放完毕），屏幕左右（或上下）平铺<b>两张图片</b>。没有边框，没有阴影，只有图。</font></p></li> 
 <li><p><b><font>孩子点击：</font></b></p> 
  <ul> 
   <li><p><font><b>点对（正确图）：</b> 播放一个轻快、微弱的正面反馈音（如“叮咚”或清脆的水滴声，切忌太长太夸张的动画），<b>延迟0.3秒，直接淡入下一题</b>。</font></p></li> 
   <li><p><font><b>点错（干扰图）：</b> 错误图轻轻左右晃动一下（抖动动效），同时<b>重新播放一遍该句子的音频</b>，给孩子纠错的机会，直到点对为止。</font></p></li> 
  </ul></li> 
 <li><p><font><b>自动过关：</b> 第15题点完，不跳出“You Win”的弹窗。直接播放一段 3 秒的轻快乐曲，屏幕渐变，自动加载下一关的第一题。</font></p></li> 
</ol> 
<h2>四、 技术层：极简独立开发实现路径</h2> 
<p><font>由于功能极其纯粹，这个项目非常适合用现代跨端框架快速撸一个 MVP（最小可行性产品）。</font></p> 
<h5><font>1. 技术栈推荐</font></h5> 
<ul> 
 <li><p><font><b>前端框架：</b> <b>Flutter</b> 或 <b>React Native</b>。</font></p> 
  <ul> 
   <li><p><font><i>理由：</i> 一套代码同时打包 iOS 和 Android（平板和手机），动画流畅，处理图片和音频非常简单。</font></p></li> 
  </ul></li> 
 <li><p><font><b>存储与数据：</b> <b>纯本地离线运行</b>。</font></p> 
  <ul> 
   <li><p><font><i>理由：</i> 启蒙App不需要搞用户注册、联网登录。把 JSON 数据、图片、音频全部打包在 App 本地（Asset）。既省去了服务器成本，又能保证极其丝滑的加载速度，还能防止孩子断网不能用。</font></p></li> 
  </ul></li> 
</ul> 
<h5><font>2. 核心代码逻辑（以 Flutter/React Native 伪代码理解）</font></h5> 
<p><font>只需要维护一个简单的状态机（State）：</font></p> 
<p><font>JavaScript</font></p> 
<pre><code><font>// 当前状态 state = { currentLevel: 1, currentQuestionIndex: 0, // 0 到 14 isPlayingAudio: false } // 核心函数：加载题目 function loadQuestion() { let q = data[currentLevel][currentQuestionIndex]; // 1. 播放音频 AudioManager.play(q.audio_path); // 2. 随机左右渲染图片（保证正确答案不总在一边） let displayImages = shuffle([q.correct_img, q.distractor_img]); renderUI(displayImages); } // 核心函数：点击处理 function handleImageClick(clickedImage) { if (clickedImage == currentQuestion.correct_img) { // 答对了 PlaySound("correct_chime"); if (currentQuestionIndex &lt; 14) { currentQuestionIndex++; setTimeout(loadQuestion, 500); // 0.5秒后自动下一题 } else { // 通关 currentLevel++; currentQuestionIndex = 0; PlaySound("level_complete"); setTimeout(loadQuestion, 2000); } } else { // 答错了 PlaySound("wrong_buzz_or_shake"); ShakeImage(clickedImage); // 图片抖动 AudioManager.play(currentQuestion.audio_path); // 重播语音 } } </font></code></pre> 
<h5><font>3. 音频（TTS）怎么搞？</font></h5> 
<p><font>既然句子是 AI 生成的，音频也不需要找真人录音。</font></p> 
<ul> 
 <li><p><font>使用 <b>OpenAI TTS (Text-to-Speech)</b> 的 <code>alloy</code> 或 <code>shimmer</code> 音色，或者使用 <b>ElevenLabs</b> 的少儿/亲和力标杆音色。</font></p></li> 
 <li><p><font>把 15 个句子批量输入，生成高质量的 <code>.mp3</code> 文件，和图片放在一起。</font></p></li> 
</ul> 
<h2>五、 项目落地行动指南（分步走）</h2> 
<p><font>如果你想立刻开始，按照这个顺序推進，大约 2-3 周就能看到 Demo：</font></p> 
<ol> 
 <li><p><b><font>第一周：内容蒸馏与微调</font></b></p> 
  <ul> 
   <li><p><font>用 ChatGPT 把前 5 关（75个句子）的 JSON 数据跑出来。</font></p></li> 
   <li><p><font>严格检查干扰项的合理性，确保非母语孩子的认知难度是循序渐进的。</font></p></li> 
  </ul></li> 
 <li><p><b><font>第二周：资产（Assets）准备</font></b></p> 
  <ul> 
   <li><p><font>把 150 张图片（75正确 + 75干扰）用 Midjourney/Flux 批量跑出来。</font></p></li> 
   <li><p><font>把 75 个句子的音频用 OpenAI TTS 导出来。</font></p></li> 
  </ul></li> 
 <li><p><b><font>第三周：组装开发</font></b></p> 
  <ul> 
   <li><p><font>如果你自己不会写代码，把你的想法、交互流程（第三部分）以及生成的 JSON 格式喂给 <b>Cursor</b> 或者 <b>Claude 3.5 Sonnet</b>。</font></p></li> 
   <li><p><font>对 AI 说：“请用 Flutter 帮我写一个极简的单页应用，逻辑如下...（贴入上述逻辑）”。让 AI 帮你写出完整的 App 代码。</font></p></li> 
  </ul></li> 
 <li><p><b><font>第四周：真机测试</font></b></p> 
  <ul> 
   <li><p><font>导进 iPad 或安卓平板。<b>一定要找个小朋友测试</b>。你会发现，最能检验“非必要按钮是否删干净”的，就是小孩子的乱点行为（看他们会不会卡死在某个界面）。</font></p></li> 
  </ul></li> 
</ol> 
<p><font><br></font></p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:47 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-102.html</guid>
</item>
<item>
    <title>为什么很多 AI Agent 一上线就开始烧钱？我在这个开源项目里看到了答案</title>
    <link>http://www.51keeplearning.com/post-101.html</link>
    <description><![CDATA[<p>嗨，我是小华同学，专注解锁高效工作与前沿AI工具！每日精选开源技术、实战技巧，助你省时50%、领先他人一步。免费订阅，与10万+技术人共享升级秘籍！</p> 
<p><img class="aligncenter" src="https://raw.githubusercontent.com/BlockRunAI/ClawRouter/main/assets/banner.png"></p> 
<blockquote> 
 <p><strong>我的判断：</strong></p> 
 <p>&nbsp;</p> 
 <p>很多 AI Agent 不是输在模型不够强，而是输在<strong>每一步都用错了模型</strong>。</p> 
 <p>&nbsp;</p> 
 <p>真正跑进生产环境以后，最麻烦的不是“能不能调通模型”，而是<strong>每一步该用哪个模型、花多少钱、失败了怎么办</strong>。</p> 
 <p>&nbsp;</p> 
 <p>ClawRouter 这个开源项目，正好在尝试解决这件事。</p> 
</blockquote> 
<hr> 
<h2>先把问题说白</h2> 
<p>如果只是做一个聊天框，模型调用很简单：</p> 
<blockquote> 
 <p>用户问一句，模型答一句。</p> 
</blockquote> 
<p>但 Agent 不一样。</p> 
<p>它会拆任务、读文件、调用工具、写代码、跑命令、修错误。一个看起来简单的目标，背后可能是十几次甚至几十次 LLM 调用。</p> 
<p>这时候就会出现一个很实际的问题：</p> 
<table> 
 <thead> 
  <tr> 
   <th>选择</th> 
   <th>结果</th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td>全用最强模型</td> 
   <td>效果稳，但成本容易爆</td> 
  </tr> 
  <tr> 
   <td>全用便宜模型</td> 
   <td>成本低，但关键步骤容易翻车</td> 
  </tr> 
  <tr> 
   <td>手写规则判断</td> 
   <td>前期能用，后期维护麻烦</td> 
  </tr> 
 </tbody> 
</table> 
<p>所以问题不是“接哪个大模型平台”，而是：</p> 
<blockquote> 
 <p><strong>模型调用本身，需不需要一层路由系统？</strong></p> 
</blockquote> 
<hr> 
<h2>ClawRouter 的答案：需要</h2> 
<p>ClawRouter 的定位，可以理解成：</p> 
<blockquote> 
 <p>在你的应用和模型供应商之间，加一个本地模型路由器。</p> 
</blockquote> 
<p>你的应用还是按 OpenAI-compatible API 调用，但请求会先经过 ClawRouter。它会判断这次任务更适合免费模型、低成本模型，还是更强的推理模型。</p> 
<p><img class="aligncenter" src="https://raw.githubusercontent.com/BlockRunAI/ClawRouter/main/docs/assets/blockrun-clawrouter-openclaw-agentic-proxy-architecture.png"></p> 
<p>它主要处理这几类问题：</p> 
<table> 
 <thead> 
  <tr> 
   <th>问题</th> 
   <th>对应能力</th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td>任务难度不同</td> 
   <td>智能路由</td> 
  </tr> 
  <tr> 
   <td>成本不可控</td> 
   <td>模型分层和预算控制</td> 
  </tr> 
  <tr> 
   <td>模型失败或限流</td> 
   <td>fallback</td> 
  </tr> 
  <tr> 
   <td>Agent 上下文越来越长</td> 
   <td>Token 压缩</td> 
  </tr> 
  <tr> 
   <td>多模型接口不统一</td> 
   <td>OpenAI-compatible API</td> 
  </tr> 
 </tbody> 
</table> 
<p>这就比普通“模型聚合器”更进一步。</p> 
<p>普通聚合器更像模型超市，ClawRouter 更像调度系统。</p> 
<hr> 
<h2>我觉得最有价值的是这 3 点</h2> 
<h3>第一，它把“选模型”变成基础设施</h3> 
<p>很多团队刚开始做 AI 产品时，都会先用最强模型兜底。</p> 
<p>这个选择很合理：先保证效果。</p> 
<p>但当调用量上来以后，就会发现不是所有任务都值得用贵模型。</p> 
<p>例如：</p> 
<ul> 
 <li>简单格式转换，不需要最强推理。</li> 
 <li>普通摘要，可以用中等模型。</li> 
 <li>复杂代码修复，才需要更强模型。</li> 
</ul> 
<p>ClawRouter 的价值是把这些判断放到路由层，而不是让业务代码到处写规则。</p> 
<p><img class="aligncenter" src="https://raw.githubusercontent.com/BlockRunAI/ClawRouter/main/docs/assets/clawrouter-14-dimension-routing-classification.png"></p> 
<h3>第二，它把失败恢复前置了</h3> 
<p>模型调用失败不是偶然事件。</p> 
<p>你可能遇到：</p> 
<ul> 
 <li>限流。</li> 
 <li>超时。</li> 
 <li>余额不足。</li> 
 <li>模型临时不可用。</li> 
</ul> 
<p>如果一个 Agent 正在执行长任务，中途某一步失败，最好不要让整个任务直接崩掉。</p> 
<p>所以 fallback 很重要。</p> 
<p>更重要的是，fallback 不应该散落在业务逻辑里，而应该成为模型调用层的一部分。</p> 
<h3>第三，它开始治理长上下文</h3> 
<p>Agent 运行越久，上下文越容易膨胀。</p> 
<p>工具返回、文件内容、错误日志、重复路径，都会慢慢堆起来。</p> 
<p>这不只是“上下文窗口够不够”的问题，也是成本问题。</p> 
<p><img class="aligncenter" src="https://raw.githubusercontent.com/BlockRunAI/ClawRouter/main/docs/assets/blockrun-clawrouter-7-layer-token-compression-openclaw.png"></p> 
<p>我更愿意把它理解成：</p> 
<blockquote> 
 <p>Agent 需要的不只是记忆力，还需要上下文卫生。</p> 
</blockquote> 
<hr> 
<h2>它适合谁关注？</h2> 
<p>我觉得这几类人可以看一下：</p> 
<table> 
 <thead> 
  <tr> 
   <th>人群</th> 
   <th>关注点</th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td>AI Agent 开发者</td> 
   <td>长任务、工具调用、失败恢复</td> 
  </tr> 
  <tr> 
   <td>AI SaaS 团队</td> 
   <td>调用成本和模型分层</td> 
  </tr> 
  <tr> 
   <td>后端工程师</td> 
   <td>模型调用如何工程化</td> 
  </tr> 
  <tr> 
   <td>开源爱好者</td> 
   <td>Agent 基础设施的新方向</td> 
  </tr> 
 </tbody> 
</table> 
<p>如果你只是做一个小 Demo，它可能暂时不是刚需。</p> 
<p>但如果你已经开始思考“模型成本怎么控”“Agent 怎么稳定跑完任务”，这类路由层会越来越重要。</p> 
<hr> 
<h2>不要把它神化</h2> 
<p>这个项目值得关注，但不等于能解决所有问题。</p> 
<p>它不是本地推理框架，底层仍然依赖外部模型服务。</p> 
<p>它的 x402 / USDC 微支付设计也比较前沿，对传统团队会有一点理解成本。</p> 
<p>所以我更建议把它当成一种架构参考：</p> 
<blockquote> 
 <p>当 LLM 调用变多以后，模型选择、成本、fallback、上下文压缩，应该从业务代码里抽出来，变成一层可复用的基础设施。</p> 
</blockquote> 
<hr> 
<h2>最后</h2> 
<p>ClawRouter 最有意思的地方，不是“又接了多少模型”。</p> 
<p>而是它提醒我们：</p> 
<blockquote> 
 <p><strong>AI Agent 不是一次性问答工具，而是会持续运行的程序。只要它持续运行，模型调用就必须被调度、被治理、被观测。</strong></p> 
</blockquote> 
<p>这也是我觉得它值得收藏的原因。</p> 
<p>后面如果继续拆，我会优先看它的路由策略、Token 压缩、fallback 机制，以及怎么接到自己的 Agent 项目里。</p> 
<p>项目地址：</p> 
<p>https://github.com/BlockRunAI/ClawRouter</p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:46 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-101.html</guid>
</item>
<item>
    <title>15天学会AI应用开发（九）利用Chroma持久化向量数据</title>
    <link>http://www.51keeplearning.com/post-100.html</link>
    <description><![CDATA[<p><span><span>​</span></span>上一篇文章通过all-MiniLM-L6-v2模型结合FAISS实现了简单的RAG检索功能，但FAISS运行于内存中，无法持久化保存向量数据，重启后又得重新对文本做向量化，比较浪费算力资源。本文就来介绍如何使用向量数据库来持久保存向量数据。 </p>
<h1>一、向量数据库Chroma</h1> 
<p>Chroma是一个本地向量数据库，专门用来保存 RAG 知识库，并且快速检索相关内容。Chroma与FAISS之间的区别如下：</p> 
<p>1、只做检索，不做保存；Chroma既支持检索，也支持保存。<br> 2、FAISS运行于内存，速度快；Chroma需要从磁盘加载数据，启动稍慢。<br> 3、FAISS适合临时检索，多用于简单RAG；Chroma支持长期使用，尤其是资料较多的RAG。</p> 
<p>总而言之，Chroma属于本地 + 简单 + 自带持久化的 RAG 专用数据库，可以理解为给大模型用的本地知识小仓库。</p> 
<h1>二、如何使用Chroma</h1> 
<p>Python代码里的Chroma主要完成三件事：创建数据库与数据表、保存知识向量、在用户提问时检索知识，如下代码所示：</p>  
<pre><code>import chromadb

# 创建数据库，指定本地的数据库路径
client = chromadb.PersistentClient(path="./chroma_db")

# 创建数据表
collection = client.get_or_create_collection("knowledge_base")

# 把 文本+向量 一起存进去
collection.add(documents=knowledge, embeddings=vectors, ids=ids)

# 提问 → 自动检索最相似的
results = collection.query(query_embeddings=q_vec, n_results=1)</code></pre>  
<p>可见Chroma的使用步骤与FAISS一一对应，如下所示：</p> 
<p>1、FAISS的第一步是建立内存索引，而Chroma的第一步是创建数据库与数据表。<br> 2、FAISS的第二步是把知识向量放入内存索引，而Chroma的第二步是把知识向量保存到数据表。<br> 3、FAISS的第三步是从内存索引查找相关知识，而Chroma的第三步是从数据表查找相关知识。</p> 
<h1>三、结合all-MiniLM-L6-v2与Chroma实现RAG</h1> 
<p>接下来演示如何使用all-MiniLM-L6-v2与Chroma实现RAG检索功能。在编写Python代码前，要先在命令行执行下面的pip安装命令：</p>  
<pre><code>pip install chromadb sentence-transformers</code></pre>  
<p>然后编写下面的Python检索测试代码：</p>  
<pre><code>from sentence_transformers import SentenceTransformer
import chromadb

# ===================== 知识库（不变） =====================
knowledge = [
&nbsp; &nbsp; "什么是AI：人工智能（AI）是一门使机器模拟人类智能的技术。",
&nbsp; &nbsp; "什么是RAG：RAG代表检索增强生成，通过检索外部知识提升大模型回答准确性。",
&nbsp; &nbsp; "本地RAG消耗Token吗：本地RAG不调用云端API，不消耗Token，完全免费。",
&nbsp; &nbsp; "FAISS是什么：FAISS是Facebook开源的向量检索库，用于本地高效检索。",
&nbsp; &nbsp; "RAG的作用：RAG让大模型能引用外部知识，避免胡说八道。"
]

# ===================== 加载本地embedding模型（不变） =====================
embed_model = SentenceTransformer("./all-MiniLM-L6-v2", device="cpu")

# 生成向量（把知识库向量化）
vectors = embed_model.encode(knowledge)

# ===================== Chroma 初始化 =====================
# 添加到 Chroma，要持久化用：
client = chromadb.PersistentClient(path="./chroma_db")

# 创建/获取集合
collection = client.get_or_create_collection(name="knowledge_base")

ids = [f"doc_{i}" for i in range(len(knowledge))]
collection.add(
&nbsp; &nbsp; documents=knowledge,
&nbsp; &nbsp; embeddings=vectors.tolist(), &nbsp;# 传入向量（也可让Chroma自己embedding）
&nbsp; &nbsp; ids=ids
)

# ===================== RAG 检索（逻辑不变） =====================
def rag(question):
&nbsp; &nbsp; print("\n 问题：" + question)
&nbsp; &nbsp; # 把问题向量化
&nbsp; &nbsp; q_vec = embed_model.encode([question])

&nbsp; &nbsp; # 从 Chroma 检索对应的知识
&nbsp; &nbsp; results = collection.query(
&nbsp; &nbsp; &nbsp; &nbsp; query_embeddings=q_vec.tolist(),
&nbsp; &nbsp; &nbsp; &nbsp; n_results=1
&nbsp; &nbsp; )
&nbsp; &nbsp; best = results["documents"][0][0]
&nbsp; &nbsp; answer = best.split("：")[-1]
&nbsp; &nbsp; print(" 答案：" + answer)
&nbsp; &nbsp; print("-" * 50)

# ===================== 运行 =====================
if __name__ == "__main__":
&nbsp; &nbsp; print("=== 本地智能 RAG 系统（Chroma版）===")
&nbsp; &nbsp; while True:
&nbsp; &nbsp; &nbsp; &nbsp; q = input("\n请输入问题（q退出）：")
&nbsp; &nbsp; &nbsp; &nbsp; if q.lower() == "q":
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break
&nbsp; &nbsp; &nbsp; &nbsp; rag(q)</code></pre>  
<p>运行上面的Python代码，根据提示输入问题“AI是什么？”，输出日志结果如下：</p>  
<pre><code>=== 本地智能 RAG 系统（Chroma版）===

请输入问题（q退出）：AI是什么？

&nbsp;问题：AI是什么？
&nbsp;答案：人工智能（AI）是一门使机器模拟人类智能的技术。
--------------------------------------------------</code></pre>  
<p>可见即使问题文本与知识库不是完全的字符串匹配（“AI是什么？”对“什么是AI”），all-MiniLM-L6-v2模型结合Chroma也能正确识别出问题意图，并给出准确的回答。</p> 
<p>本系列的AI应用开发文章目录为《<span>15天学会AI应用开发全目录（零基础小白，零Token消耗）》。</span>​</p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:46 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-100.html</guid>
</item>
<item>
    <title>DeepSeek 表格怎么导出到 Excel 或 Word？复制后错列怎么办</title>
    <link>http://www.51keeplearning.com/post-99.html</link>
    <description><![CDATA[<h1>DeepSeek 表格怎么导出到 Excel 或 Word？复制后错列怎么办</h1> 
<p>很多人让 DeepSeek 生成表格后，第一反应是直接复制到 Word 或 Excel。短表格通常没问题，但只要列数多、单元格里有换行、竖线、编号、链接，复制后就可能变成一整段文字，或者列和列对不上。</p> 
<p>这类问题的核心不是 DeepSeek “不会做表格”，而是 AI 输出的表格经常是 Markdown 表格，Excel 和 Word 需要的是结构化表格对象。中间少了转换步骤，就容易错列。</p> 
<h2>先判断你的表格属于哪一种</h2> 
<table> 
 <thead> 
  <tr> 
   <th>表格类型</th> 
   <th>常见表现</th> 
   <th>推荐处理</th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td>简单三五列</td> 
   <td>复制后还能看出行列</td> 
   <td>可直接复制后微调</td> 
  </tr> 
  <tr> 
   <td>Markdown 表格</td> 
   <td>有 `</td> 
   <td><code>和</code>---`</td> 
  </tr> 
  <tr> 
   <td>多行单元格</td> 
   <td>一个格子里有换行</td> 
   <td>优先转 Excel 检查</td> 
  </tr> 
  <tr> 
   <td>多张表</td> 
   <td>一次回答里有几张表</td> 
   <td>分表导出或分 sheet</td> 
  </tr> 
  <tr> 
   <td>要进正式文档</td> 
   <td>要放进 Word/PDF</td> 
   <td>先整理结构再导出</td> 
  </tr> 
 </tbody> 
</table> 
<h2>方法一：短表格直接复制，再在 Word 里转换</h2> 
<p>如果表格很短，可以把 DeepSeek 输出内容复制到 Word。若粘贴后还是 <code>|</code> 和 <code>---</code>，说明 Word 没有把它识别成表格，需要手动整理。</p> 
<p>适合谁：临时文档、少量字段、对排版要求不高。</p> 
<p>注意点：复制前尽量让 DeepSeek 输出“标准 Markdown 表格”，不要在单元格里写太长的段落，也不要混入多余说明。</p> 
<h2>方法二：先让 AI 输出 CSV，再导入 Excel</h2> 
<p>如果目标是 Excel，建议让 DeepSeek 重新输出 CSV 格式。然后在 Excel 里通过“数据”里的文本或 CSV 导入功能打开。</p> 
<p>可以这样问：</p> 
<pre><code>请把上面的表格改成 CSV 格式，第一行保留字段名，不要额外解释。
</code></pre> 
<p>适合谁：需要筛选、排序、计算、继续编辑的人。</p> 
<p>注意点：中文逗号、英文逗号、单元格里的换行都可能影响导入。导入后要检查列数、表头、日期、数字和前导零。</p> 
<h2>方法三：把 Markdown 表格先转成真正表格</h2> 
<p>DeepSeek 常见表格长这样：</p> 
<pre><code>| 项目 | 数值 | 说明 |
| --- | --- | --- |
| A | 10 | 正常 |
</code></pre> 
<p>这种结构对 Markdown 阅读器友好，但 Word/Excel 不一定会直接识别。更稳的做法是先用 Markdown 编辑器、Pandoc 或表格转换工具把它变成 docx、xlsx 或 HTML 表格。</p> 
<p>适合谁：表格不止一张、列比较多、要进入正式报告。</p> 
<p>注意点：Markdown 表格要求表头和分隔行完整，管道符表示列边界。如果某个单元格内部也出现 <code>|</code>，就可能被误判成新列。</p> 
<h2>方法四：用 DS随心转整理 AI 表格再导出</h2> 
<p>如果表格来自 DeepSeek、豆包、Kimi、ChatGPT 等 AI 回答，而且要放进 Word、PDF 或 Excel，建议不要只做“复制粘贴”，而是先做一次结构化整理。</p> 
<p>DS随心转适合处理 AI 回答到正式文档的最后一步交付。它覆盖网页端、浏览器插件、App 和小程序，可以把 AI 回答中的标题、表格、代码块、公式等内容整理后导出为 Word、PDF、Excel、图片或长图。</p> 
<p>它不是替代 DeepSeek 的聊天工具，而是把 AI 生成内容变成可交付文档的工作台。高频处理网页里的 AI 回答时，可以考虑浏览器插件入口；临时处理也可以用 Web、App 或小程序。</p> 
<h2>方案对比</h2> 
<table> 
 <thead> 
  <tr> 
   <th>方案</th> 
   <th>适合场景</th> 
   <th>优点</th> 
   <th>注意点</th> 
  </tr> 
 </thead> 
 <tbody> 
  <tr> 
   <td>直接复制</td> 
   <td>小表格、临时使用</td> 
   <td>最快</td> 
   <td>容易出现竖线和错列</td> 
  </tr> 
  <tr> 
   <td>CSV 导入 Excel</td> 
   <td>要计算、排序、筛选</td> 
   <td>Excel 友好</td> 
   <td>需要检查分隔符</td> 
  </tr> 
  <tr> 
   <td>Markdown 转换</td> 
   <td>技术文档、报告</td> 
   <td>结构更完整</td> 
   <td>需要工具或命令</td> 
  </tr> 
  <tr> 
   <td>DS随心转</td> 
   <td>AI 长回答、多表格、多格式交付</td> 
   <td>Word/Excel/PDF/图片都能纳入同一流程</td> 
   <td>导出后仍建议抽样校对</td> 
  </tr> 
 </tbody> 
</table> 
<h2>常见问题</h2> 
<h3>DeepSeek 表格为什么复制到 Excel 只有一列？</h3> 
<p>通常是因为复制的是 Markdown 文本，而不是 Excel 表格对象。可以先让 DeepSeek 输出 CSV，再用 Excel 的文本或 CSV 导入功能处理。</p> 
<h3>表格复制到 Word 后为什么只剩竖线？</h3> 
<p>因为 Word 没有把 Markdown 的 <code>|</code> 识别为表格边界。可以先转换成 HTML、docx 或真正表格，再放进 Word。</p> 
<h3>单元格里有换行怎么办？</h3> 
<p>如果单元格里有换行，直接复制最容易错列。建议先转 Excel 检查，再决定是否放入 Word。</p> 
<h3>多张表可以一次导出吗？</h3> 
<p>可以，但要先分清表格边界。多张表最好分标题、分 sheet 或分段导出，避免混成一张大表。</p> 
<h3>DS随心转适合什么情况？</h3> 
<p>适合 AI 回答里同时有标题、表格、公式、代码块、多段解释，并且最终要交付 Word、PDF、Excel 或长图的情况。</p> 
<h2>参考资料与延伸阅读</h2> 
<ul> 
 <li>GitHub Docs：Organizing information with tables</li> 
 <li>GitHub Flavored Markdown Spec：Tables extension</li> 
 <li>Microsoft Support：Import or export text .txt or .csv files</li> 
 <li>Pandoc User's Guide</li> 
 <li>DS随心转产品知识库与实际使用经验</li> 
</ul> 
<p>本文为工具使用经验整理，部分内容由 AI 辅助生成，并已人工校对。</p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:46 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-99.html</guid>
</item>
<item>
    <title>15天学会AI应用开发（十）把文本嵌入模型换成国产模型</title>
    <link>http://www.51keeplearning.com/post-98.html</link>
    <description><![CDATA[<p>前面两篇文章在演示RAG功能时，做向量化的文本嵌入模型都用国外的all-MiniLM-L6-v2，该模型主要适用英文，对于中文总体也能用，但在细节上处理欠佳。本文就来介绍如何使用国产离线的文本嵌入模型替换国外模型，以及如何体现国产模型的比较优势。 </p>
<h1>一、all-MiniLM-L6-v2的缺点</h1> 
<p>虽然all-MiniLM-L6-v2的优点很多，比如下列几点：</p> 
<p>1、体积极小：几十 MB，随便本地离线下载<br> 2、速度超快：CPU 就能跑，不用显卡<br> 3、通用好用：英文很强，中文也能用（日常 RAG 足够）<br> 4、适配所有向量库：FAISS、Chroma、Milvus 全都兼容</p> 
<p>但是all-MiniLM-L6-v2的缺点也很明显，就是它对中文的理解能力一般，有时会出现一些似是而非的错误。</p> 
<p>比如之前的文章《15天学会AI应用开发（八）使用向量数据库实现RAG功能》，运行Python测试代码时，如果输入的问题是“RAG是什么”，输出日志结果如下：</p>  
<pre><code>=== 本地智能 RAG 系统：问什么答什么 ===

请输入问题（q退出）：RAG是什么

&nbsp;问题：RAG是什么
&nbsp;答案：RAG让大模型能引用外部知识，避免胡说八道。
--------------------------------------------------</code></pre>  
<p>然而原始的知识库是这样的：</p>  
<pre><code>&nbsp; &nbsp; "什么是AI：人工智能（AI）是一门使机器模拟人类智能的技术。",
&nbsp; &nbsp; "什么是RAG：RAG代表检索增强生成，通过检索外部知识提升大模型回答准确性。",
&nbsp; &nbsp; "本地RAG消耗Token吗：本地RAG不调用云端API，不消耗Token，完全免费。",
&nbsp; &nbsp; "FAISS是什么：FAISS是Facebook开源的向量检索库，用于本地高效检索。",
&nbsp; &nbsp; "RAG的作用：RAG让大模型能引用外部知识，避免胡说八道。"</code></pre>  
<p>可见问题“RAG是什么”的正确答案是第二条知识“RAG代表检索增强生成，通过检索外部知识提升大模型回答准确性。”，但all-MiniLM-L6-v2返回的答案却是最后一条知识“RAG让大模型能引用外部知识，避免胡说八道。”，说明all-MiniLM-L6-v2对中文的支持不够精准。</p> 
<p>对于专业中文的RAG来说，更好的办法是换成国产的文本嵌入模型，比如BGE-small、BGE-base、Qwen-Embedding等等。</p> 
<h1>二、下载中文嵌入模型BGE-small</h1> 
<p>BGE-small的模型页面为 https://modelscope.cn/models/BAAI/bge-small-zh-v1.5 ，模型文件大小为192.25MB。</p> 
<p>在下载离线大模型前，要先在命令行执行下面的pip安装命令：</p>  
<pre><code>pip install modelscope</code></pre>  
<p>接着命令行通过cd命令进入Python工程的所在目录，再执行下面的模型下载命令：</p>  
<pre><code>modelscope download --model BAAI/bge-small-zh-v1.5 --local_dir bge-small-zh-v1.5</code></pre>  
<p>上面下载命令的“--model”参数表示离线大模型为“BAAI/bge-small-zh-v1.5”，而“--local_dir”参数表示离线大模型的本地保存目录。</p> 
<p>下载完毕，即可在Python工程的目录下方找到bge-small-zh-v1.5文件夹，里面保存的便是BGE-small的具体模型文件。</p> 
<h1>三、结合BGE-small与FAISS实现RAG</h1> 
<p>接下来演示如何使用BGE-small与FAISS实现RAG检索功能。在编写Python代码前，要先在命令行执行下面的pip安装命令：</p>  
<pre><code>pip install faiss-cpu sentence-transformers</code></pre>  
<p>然后编写下面的Python检索测试代码，与之前教程的代码相比，仅仅把all-MiniLM-L6-v2改成了bge-small-zh-v1.5：</p>  
<pre><code>from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

# ===================== 你的知识库（完全不变） =====================
knowledge = [
&nbsp; &nbsp; "什么是AI：人工智能（AI）是一门使机器模拟人类智能的技术。",
&nbsp; &nbsp; "什么是RAG：RAG代表检索增强生成，通过检索外部知识提升大模型回答准确性。",
&nbsp; &nbsp; "本地RAG消耗Token吗：本地RAG不调用云端API，不消耗Token，完全免费。",
&nbsp; &nbsp; "FAISS是什么：FAISS是Facebook开源的向量检索库，用于本地高效检索。",
&nbsp; &nbsp; "RAG的作用：RAG让大模型能引用外部知识，避免胡说八道。"
]

# ===================== 加载你本地已有的模型（不变） =====================
embed_model = SentenceTransformer("./bge-small-zh-v1.5", device="cpu")

# 生成向量（把知识库向量化）
vectors = embed_model.encode(knowledge)

# 构建 FAISS 索引
index = faiss.IndexFlatL2(vectors.shape[1])
index.add(np.array(vectors).astype("float32"))

# ===================== RAG 检索（逻辑不变） =====================
def rag(question):
&nbsp; &nbsp; print("\n 问题：" + question)
&nbsp; &nbsp; # 把问题向量化
&nbsp; &nbsp; q_vec = embed_model.encode([question])
&nbsp; &nbsp; # 从 FAISS 检索对应的知识
&nbsp; &nbsp; D, I = index.search(np.array(q_vec).astype("float32"), 1)
&nbsp; &nbsp; best = knowledge[I[0][0]]
&nbsp; &nbsp; answer = best.split("：")[-1]
&nbsp; &nbsp; print(" 答案：" + answer)
&nbsp; &nbsp; print("-" * 50)

# ===================== 运行 =====================
if __name__ == "__main__":
&nbsp; &nbsp; print("=== 本地智能 RAG 系统：问什么答什么 ===")
&nbsp; &nbsp; while True:
&nbsp; &nbsp; &nbsp; &nbsp; q = input("\n请输入问题（q退出）：")
&nbsp; &nbsp; &nbsp; &nbsp; if q.lower() == "q":
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break
&nbsp; &nbsp; &nbsp; &nbsp; rag(q)</code></pre>  
<p>运行上面的Python代码，根据提示先输入问题“RAG是什么？”，再输入问题“RAG有哪些作用？”，输出日志结果如下：</p>  
<pre><code>=== 本地智能 RAG 系统：问什么答什么 ===

请输入问题（q退出）：RAG是什么？

&nbsp;问题：RAG是什么？
&nbsp;答案：RAG代表检索增强生成，通过检索外部知识提升大模型回答准确性。
--------------------------------------------------

请输入问题（q退出）：RAG有哪些作用？

&nbsp;问题：RAG有哪些作用？
&nbsp;答案：RAG让大模型能引用外部知识，避免胡说八道。
--------------------------------------------------</code></pre>  
<p>可见在回答问题“RAG是什么？”的时候，BGE-small正确返回了第二条知识“RAG代表检索增强生成，通过检索外部知识提升大模型回答准确性。”。<br> 在回答问题“RAG有哪些作用？”的时候，BGE-small也正确返回了最后一条知识“RAG让大模型能引用外部知识，避免胡说八道。”</p> 
<p>由此说明，中文嵌入模型确实比国外的all-MiniLM-L6-v2拥有更准确的中文检索结果。</p> 
<p>本系列的AI应用开发文章目录为《<span>15天学会AI应用开发全目录（零基础小白，零Token消耗）》。</span></p> 
<p><span> <span>​</span></span></p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:45 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-98.html</guid>
</item>
<item>
    <title>Runlet：一个小而清晰、Provider Neutral 的 Python Agent Runtime</title>
    <link>http://www.51keeplearning.com/post-97.html</link>
    <description><![CDATA[<p>调用模型很简单，真正麻烦的是把它做成一个稳定、可控、可观测的<br>runtime。</p> 
<p>大多数 Python 开发者都可以在几分钟内完成第一个 demo：发一个请求，<br>拿到模型回复，打印结果。真正的问题通常从下一步才开始。当一个简单的<br>模型调用逐渐演变成真实应用里的 agent runtime，你很快会碰到流式输出、<br>工具调用、多 Provider 差异、上下文控制，以及线上出了问题之后到底发生了<br>什么这些事情。</p> 
<p><strong>Runlet</strong> 就是在这样的背景下做出来的。</p> 
<p>Runlet 是一个轻量、provider neutral 的 Python agent runtime library。<br>它不是托管平台，不是工作流引擎，也不是一整套应用框架。它更像一层专注<br>的运行时边界，给那些希望在自己 Python 应用里明确掌控 agent 执行过程的<br>开发者使用。</p> 
<h2>真正麻烦的，通常不是第一个 demo</h2> 
<p>第一个 demo 一般都不难。</p> 
<p>难的是后面的事情：</p> 
<ul> 
 <li>你想同时支持多个模型提供方</li> 
 <li>你需要在同一条执行链路里处理 streaming 和 tool calling</li> 
 <li>你希望在模型调用前统一处理 request shaping 和 context control</li> 
 <li>你需要结构化事件来支撑调试、日志和 observability</li> 
 <li>你需要 state，但又不希望 runtime 库悄悄膨胀成一个 memory framework</li> 
</ul> 
<p>很多 Python agent 方案都很擅长帮助你快速起步。但一旦你开始认真看<br>runtime 这一层，取舍就会变得很明显。有些方案和某一家 provider 的 API<br>耦合得很深；有些方案编排能力很强，但对想嵌入现有应用的团队来说太重；<br>有些方案让 happy path 看起来很顺，但 stream、tool loop 和 observability<br>最后还是散落在应用代码里。</p> 
<p>我们想要的是一个更小、更清楚的答案。</p> 
<h2>我们反复遇到的四个 runtime 问题</h2> 
<h3>1. Provider 差异会慢慢渗透到整个应用里</h3> 
<p>Provider 耦合通常不会只停留在网络客户端这一层。它会继续出现在消息格式、<br>tool call payload、stream 事件、reasoning / thinking 字段、request<br>options，甚至调用代码本身的形状里。一旦发生这种事，应用的其他部分也会<br>开始继承 provider 特有的假设。</p> 
<p>在一次性原型里，这还算能忍。到了稍微长寿一点的系统里，这件事就会变得<br>越来越别扭。</p> 
<p>Runlet 的思路是把 provider 视为 adapter，而不是把某个 provider 的 API<br>当成整个 runtime 的事实标准。核心 runtime 只处理自己的消息、请求、响应、<br>事件和工具调用契约。具体 provider 再去完成 Runlet runtime model 与 SDK<br>之间的翻译。</p> 
<p>这听起来不复杂，但它很重要。它意味着即便不同 provider 暴露出来的 API<br>风格差异很大，runtime 本身依然可以保持稳定。</p> 
<h3>2. Streaming 和 tool execution 真正难的是运行时控制流</h3> 
<p>流式输出文本很容易演示。流式执行一个 agent，并不容易。</p> 
<p>一旦工具进入链路，模型的流式输出就不再只是文本。它可能包含部分文本、<br>部分工具参数、一个完整的 tool call、工具执行结果，然后再进入下一轮模型<br>调用。直到最后拿到终态回复之前，这其实是一段持续演化的运行时控制流。</p> 
<p>这时候应用代码很容易开始长出一堆临时逻辑：</p> 
<ul> 
 <li>读取 delta</li> 
 <li>识别是不是 tool call</li> 
 <li>拼装参数</li> 
 <li>执行工具</li> 
 <li>把工具结果补回上下文</li> 
 <li>再次调用模型</li> 
 <li>持续循环，直到真正结束</li> 
</ul> 
<p>Runlet 把这段 streaming tool execution loop 收回到 runtime 内部。Provider<br>负责发出 provider-neutral 的 stream event，runtime 负责执行工具、追加<br>tool result，并继续推进下一轮模型调用，直到整个 run 完成。</p> 
<p>这类循环不应该在每个应用里都重新写一遍。</p> 
<h3>3. Context control 很容易散落到应用各层</h3> 
<p>很多系统一开始都会把 context control 当成“后面再处理”的问题。结果到了<br>后面，它往往会演变成这样：</p> 
<ul> 
 <li>prompt building 在一个地方</li> 
 <li>request trimming 在另一个地方</li> 
 <li>provider-specific options 又在别的层</li> 
 <li>处理 context overflow 的紧急修补散落在各个 handler 里</li> 
</ul> 
<p>Runlet 把 context preparation 当成 runtime execution 的一部分，而不是一个<br>可有可无的辅助函数。模型请求在真正发出去之前，会先经过 runtime 的准备<br>流程。目标不是把所有策略都做成统一标准，而是先把 runtime 边界明确下来。</p> 
<p>我们也为 provider-specific request settings 选了一个比较克制的扩展点：<br><code>ModelRequest.options[...]</code>。</p> 
<p>这意味着连接级别的配置，比如 <code>api_key</code>、<code>base_url</code>、自定义 SDK client，<br>仍然放在 provider 构造参数里；而请求级别的行为则留在 request 对象上。这<br>是一个很小的设计决定，但随着 provider 增加，它能让 API 保持整洁。</p> 
<h3>4. Observability 往往加得太晚</h3> 
<p>即便系统很小，agent runtime 也已经不太容易调试了。如果没有结构化事件，<br>事情只会更快变糟。</p> 
<p>你通常会想知道：</p> 
<ul> 
 <li>一次 run 是什么时候开始的</li> 
 <li>最终实际送去给模型的输入是什么</li> 
 <li>context preparation 有没有修改请求</li> 
 <li>模型是什么时候被调用的</li> 
 <li>哪个工具被调用了</li> 
 <li>工具返回了什么</li> 
 <li>这次 run 是不是因为某个策略而提前停止</li> 
</ul> 
<p>如果这些可见性不是从 runtime 设计阶段就带进去，团队最后往往只能靠日志、<br>tracing wrapper 或特殊分支补丁把它拼出来。</p> 
<p>Runlet 采用的是 event-first 的思路。Run、模型调用、streaming delta、<br>tool execution、完成事件，都会发出结构化事件。Hooks 用来扩展行为，<br>Observers 用来看见发生了什么。把这两类职责分开，有助于避免 observability<br>反过来变成隐式控制流。</p> 
<p>Runlet 的 runtime 边界大致是这样：</p> 
<p><img class="aligncenter" src="https://img2024.cnblogs.com/blog/675887/202606/675887-20260628133412274-1784347023.jpg"></p> 
<h2>Runlet 想做得不一样的地方</h2> 
<p>Runlet 是刻意保持“小”的。</p> 
<p>我们的目标不是去和所有 agent framework、orchestration engine 或托管平台<br>竞争。Runlet 想提供的是一层容易嵌入、行为明确、边界克制的 runtime layer。</p> 
<p>到目前为止，这意味着：</p> 
<ul> 
 <li>一个 provider-neutral 的 runtime loop</li> 
 <li>streaming 支持</li> 
 <li>非流式和流式路径下都由 runtime 接管的 tool execution</li> 
 <li>结构化事件</li> 
 <li>模型调用和工具执行周围的 hook points</li> 
 <li>轻量的 state primitives</li> 
 <li>request-level provider options</li> 
</ul> 
<p>Runlet 提供 state store primitives，但没有直接内置一套 conversation memory<br>framework。这是有意为之。不同应用对 memory、retention、summarization、<br>session policy 的要求差别很大。相比过早塞进一套一刀切的 memory 方案，我们<br>更愿意先暴露干净的基础原语，把上层策略留在应用侧。</p> 
<h2>Runlet 现在已经能做什么</h2> 
<p>Runlet 还在 beta 阶段，但已经可以拿来做真实的集成工作。</p> 
<p>当前内置 provider 包括：</p> 
<ul> 
 <li><code>OpenAIChatCompletionsProvider</code></li> 
 <li><code>OpenAIResponsesProvider</code></li> 
 <li><code>AnthropicMessagesProvider</code></li> 
</ul> 
<p>当前 runtime 能力包括：</p> 
<ul> 
 <li>非流式 completion</li> 
 <li>流式文本输出</li> 
 <li>tool calling</li> 
 <li>streaming tool execution</li> 
 <li>provider 支持时的 reasoning / thinking 输出</li> 
 <li>结构化 runtime events</li> 
 <li>轻量 state primitives</li> 
</ul> 
<p>落到工程实践里，这意味着你已经可以用 Runlet 承接那一部分大多数项目迟早都<br>得自己实现的能力：围绕模型调用的 runtime 行为本身。</p> 
<h2>Runlet 不打算做什么</h2> 
<p>Runlet 不打算变成下面这些东西：</p> 
<ul> 
 <li>托管式 agent 平台</li> 
 <li>Web 框架</li> 
 <li>图工作流引擎</li> 
 <li>UI 或 trace viewer</li> 
 <li>完整 memory framework</li> 
</ul> 
<p>如果你想要的是一个自带 orchestration、persistence、retrieval 和应用脚手架<br>的一体化系统，那 Runlet 刻意没有往那个方向走。</p> 
<p>这是一种设计选择，不是功能列表还没补齐。</p> 
<h2>Runlet 适合谁</h2> 
<p>如果你符合下面这些情况，Runlet 会比较合适：</p> 
<ul> 
 <li>你在构建一个需要嵌入 agent runtime 的 Python 应用</li> 
 <li>你希望把 provider 选择控制在 adapter 边界之后</li> 
 <li>你要的是一层小而清晰的 runtime，而不是完整平台</li> 
 <li>你愿意自己掌控应用层的 state 和 memory 策略</li> 
</ul> 
<p>如果你更期待下面这些东西，Runlet 可能就不是最合适的选择：</p> 
<ul> 
 <li>一个 batteries-included 的托管 agent 平台</li> 
 <li>一个 graph-first 的 orchestration 模型</li> 
 <li>内置好的产品级 memory 和 retrieval 抽象</li> 
</ul> 
<h2>一个最小示例</h2>  
<pre><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> asyncio
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> os

</span><span style="color: rgba(0, 0, 255, 1)">from</span> dotenv <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> load_dotenv

</span><span style="color: rgba(0, 0, 255, 1)">from</span> runlet <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> Agent, Runtime
</span><span style="color: rgba(0, 0, 255, 1)">from</span> runlet.providers <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> OpenAIChatCompletionsProvider

async </span><span style="color: rgba(0, 0, 255, 1)">def</span> main() -&gt;<span style="color: rgba(0, 0, 0, 1)"> None:
    load_dotenv()

    provider </span>=<span style="color: rgba(0, 0, 0, 1)"> OpenAIChatCompletionsProvider(
        model</span>=os.environ.get(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">OPENAI_MODEL</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">gpt-4o-mini</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">),
        api_key</span>=os.environ[<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">OPENAI_API_KEY</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">],
        base_url</span>=os.environ.get(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">OPENAI_BASE_URL</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">),
    )

    agent </span>=<span style="color: rgba(0, 0, 0, 1)"> Agent(
        name</span>=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">assistant</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
        instructions</span>=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Be concise and helpful.</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
        model</span>=<span style="color: rgba(0, 0, 0, 1)">provider,
    )

    result </span>= await Runtime().run(agent, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Introduce Runlet in one sentence.</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
    </span><span style="color: rgba(0, 0, 255, 1)">print</span><span style="color: rgba(0, 0, 0, 1)">(result.output)

asyncio.run(main())</span></pre>  
<p>这个例子的重点并不是“调用模型有多难”。重点在于：同一个 runtime 边界可以<br>自然扩展到 streaming、tools、provider-specific request options、<br>structured events，以及应用自己管理的 state，而不需要中途把运行时模型<br>推倒重来。</p> 
<h2>试试 Runlet</h2> 
<p>如果你需要的正是这样一种 runtime 形状，可以从这里开始：</p> 
<ul> 
 <ul> 
  <li>GitHub: <a href="https://github.com/DMIAOCHEN/runlet" target="_blank" rel="nofollow">https://github.com/DMIAOCHEN/runlet</a></li> 
  <li>Docs: <a href="https://dmiaochen.github.io/runlet/" target="_blank" rel="nofollow">https://dmiaochen.github.io/runlet/</a></li> 
  <li>PyPI: <a href="https://pypi.org/project/runlet/" target="_blank" rel="nofollow">https://pypi.org/project/runlet/</a></li> 
 </ul> 
</ul> 
<p>&nbsp;</p>]]></description>
    <pubDate>Sun, 28 Jun 2026 20:57:45 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>http://www.51keeplearning.com/post-97.html</guid>
</item></channel>
</rss>