<?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[SayMeeveTime]]></title> 
<atom:link href="https://blog.sayyou.icu/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[about a man's journey through life.]]></description>
<link>https://blog.sayyou.icu/</link>
<language>zh-cn</language>

<item>
    <title>三件事：博客迁移、Caddy 部署、ChatGPT 账号池搭建</title>
    <link>https://blog.sayyou.icu/?post=34</link>
    <description><![CDATA[<h2>写在前面</h2>
<p>今天干了三件事。第一件，把博客从阿里云迁移到海外服务器。第二件，用 Docker + Caddy 重新部署博客。第三件，搭了一套 ChatGPT 多账号反代系统，让多个 GPT 账号通过 CLIProxyAPI + Antigravity 统一出口，接入 ChatWise 和 Hermes 使用。</p>
<p>整个过程用了 <strong>Codex + Computer Use</strong> 来辅助操作，算是一次比较完整的 AI 辅助运维体验。记录一下。<br />
<img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/img/20260517001929835.png" alt="Computer Use" /></p>
<hr />
<h2>一、博客迁移：从阿里云到海外服务器</h2>
<h3>为什么搬</h3>
<p>原因很简单：<strong>SSL 证书要钱</strong>。</p>
<p>阿里云的免费 SSL 证书政策收紧之后，续期和新申请都需要付费。一个技术博客，一年花几百块买证书，不值得。海外服务器配合 Caddy 或者 Cloudflare，HTTPS 证书自动签发、自动续期，零成本。</p>
<p>迁移的动机就这一个，没有更复杂的理由。</p>
<h3>迁移做了什么</h3>
<p>核心三步：</p>
<ol>
<li><strong>数据导出</strong> —— 博客数据、配置文件、静态资源从阿里云打包拉下来。如果用的是 Hugo / Hexo 这类静态博客，源文件本身就在 Git 仓库里，这一步几乎零成本。</li>
<li><strong>域名切换</strong> —— 域名托管在 <strong>Cloudflare</strong> 上，直接改 DNS 记录，把 A 记录指向新服务器 IP。Cloudflare 的 DNS 生效速度很快，改完几分钟内就能解析到新机器。同时 Cloudflare 自带的 CDN 和 DDoS 防护也一并生效，不需要额外配置。</li>
<li><strong>环境重建</strong> —— 在新服务器上用 Docker 部署博客，Caddy 做反代。下面展开讲。</li>
</ol>
<hr />
<h2>二、Docker + Caddy 部署博客</h2>
<h3>Docker 部署：数据映射到宿主机</h3>
<p>博客跑在 Docker 容器里，但所有持久化数据——文章内容、配置文件、数据库（如果有的话）——全部通过 volume 映射到宿主机目录。</p>
<pre><code class="language-yaml">version: "3.8"
services:
  blog:
    image: your-blog-image
    container_name: blog
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./data:/app/data
      - ./config:/app/config</code></pre>
<p>这样做的好处是：容器随时可以删掉重建，数据不丢。备份也方便，直接打包宿主机上的目录就行，不需要进容器里操作。</p>
<h3>Caddy：本机安装，不走 Docker</h3>
<p>Caddy 没有放进 Docker，而是<strong>直接装在宿主机上</strong>。</p>
<p>原因是 Caddy 作为最外层的反向代理，需要监听 80 和 443 端口。如果也放进容器，端口映射、容器间网络通信、证书存储都要额外处理，反而增加复杂度。本机装一个 Caddy，管理起来最直接。</p>
<p>安装：</p>
<pre><code class="language-bash">sudo apt install -y caddy</code></pre>
<h3>为什么选 Caddy 不选 Nginx</h3>
<p>就一个原因：<strong>配置文件简单</strong>。</p>
<p>同样是把 <code>blog.example.com</code> 反代到本地 8080 端口，Caddy 的配置：</p>
<pre><code>blog.example.com {
    reverse_proxy localhost:8080
}</code></pre>
<p>三行，写完了。Caddy 会自动向 Let's Encrypt 申请 SSL 证书，自动续期，自动把 HTTP 重定向到 HTTPS。不需要你操心任何证书相关的事情。</p>
<p>Nginx 做同样的事，需要写 <code>server</code> 块、配 <code>ssl_certificate</code> 路径、配 <code>location</code> 代理、配 HTTP 到 HTTPS 的 301 重定向——十几二十行配置，还得单独跑 certbot 管理证书。</p>
<p>对于个人博客这种场景，Caddy 的简洁是碾压性的优势。</p>
<h3>域名：托管在 Cloudflare</h3>
<p>域名的 DNS 托管在 Cloudflare 上。Cloudflare 和 Caddy 配合使用时有一个细节需要注意：如果 Cloudflare 开启了代理模式（橙色云朵），Caddy 自动申请证书时可能会遇到验证失败的问题，因为 Cloudflare 的代理会拦截 ACME 的 HTTP-01 验证请求。</p>
<p>两种解法：一是把 Cloudflare 的代理模式关掉，<strong>改成仅 DNS（灰色云朵）</strong>，让 Caddy 自己管证书；二是保持 Cloudflare 代理开启，用 Cloudflare 提供的 Origin Certificate，Caddy 端配置这张证书。</p>
<p>我选的第一种，简单直接。</p>
<hr />
<h2>三、CLIProxyAPI：多 ChatGPT 账号反代</h2>
<p>这是今天最折腾但也最有价值的一件事。</p>
<h3>问题背景</h3>
<p>手上有多个 ChatGPT 和Antigravity 账号。直接用的话，每个账号需要单独管理、单独配置到不同的客户端里。账号多了之后，管理成本很高，而且负载不均衡——一个号用到限速了，另一个还闲着。</p>
<h3>解决方案</h3>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/img/20260517002713877.png" alt="CLI Proxy API" /></p>
<p><strong><a href="https://github.com/router-for-me/CLIProxyAPI">CLIProxyAPI</a></strong> 做的事情是：把多个 ChatGPT 账号汇聚成一个统一的 API 入口。它在本地起一个代理服务，对外暴露标准的 OpenAI API 格式，对内管理多个账号的 token 轮换和负载均衡。</p>
<p>再配合 <strong>Antigravity</strong> 做反代，整个链路：</p>
<pre><code>ChatWise / Hermes（客户端）
        ↓
   Antigravity（反代层）
        ↓
   CLIProxyAPI（账号池管理）
        ↓
   多个 ChatGPT 账号</code></pre>
<h3>部署过程</h3>
<pre><code class="language-bash">git clone https://github.com/router-for-me/CLIProxyAPI.git
cd CLIProxyAPI</code></pre>
<p>按照项目文档配置好账号信息后启动服务，然后在 Antigravity 中配置反代规则，将请求转发到 CLIProxyAPI 的监听端口。</p>
<p>最后在 <strong>ChatWise</strong> 和 <strong>Hermes</strong> 的 API 设置中，把 Base URL 指向 Antigravity 的地址，API Key 填 CLIProxyAPI 分配的统一 key。</p>
<p>配置完成后，客户端发出的每一个请求，都会被 CLIProxyAPI 自动分发到不同的 ChatGPT 账号上。单个账号触发限速时，自动切换到下一个。对客户端完全透明。</p>
<h3>为什么接入 ChatWise 和 Hermes</h3>
<p><strong>ChatWise</strong> 的优势在于多模型管理和 MCP 工具生态，适合日常对话和工具调用。<strong>Hermes</strong> 更轻量，适合快速提问和移动端使用。两个客户端接入同一个反代入口，共享同一个账号池，使用体验统一且稳定，但也只是做备用。</p>
<hr />
<h2>回顾</h2>
<p>今天这三件事串起来看，做的其实是同一件事：<strong>用尽量低的成本，把自己的工具链搭稳</strong>。</p>
<p>阿里云 SSL 要收费，那就换个不收费的方案。Nginx 配置太啰嗦，那就换 Caddy。多个 ChatGPT 账号管理太分散，那就建一个统一入口。</p>
<p>没有什么高深的技术，都是现成的工具拼在一起。但拼的过程本身，就是对自己工作流的一次梳理。</p>
<hr />
<p><em>本文部署过程中使用 Codex + Computer Use 辅助完成服务器操作。</em></p>]]></description>
    <pubDate>Sun, 17 May 2026 00:24:59 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=34</guid>
</item>
<item>
    <title>&quot;AI&quot; 吞噬一切,</title>
    <link>https://blog.sayyou.icu/?post=31</link>
    <description><![CDATA[<h2>从 OpenClaw 到豆包手机：端到端 AI，正在重写互联网的行为逻辑</h2>
<p>这两年 AI 看起来喧闹，其实真正重要的变化几乎没有声音。</p>
<p>它不在参数规模，也不在模型榜单，而是在一个更底层、但决定命运的地方发生：<br />
AI 正在从“给你建议”，变成“替你行动”。</p>
<p>从 OpenClaw 这样的本地主权 Agent，到 Claude Code、OpenCode 这一代编码系统，再到饱受争议的豆包手机，其实是一条非常清晰的演进路径——<br />
Agent 正在从对话层，进入系统控制层。</p>
<p>一旦这件事成立，过去二十年的互联网商业逻辑，几乎都会失效。</p>
<p>⸻</p>
<h2>主权 AI：OpenClaw 不是聊天工具，是控制权回收</h2>
<p>OpenClaw 的价值，并不在于它“能不能聊”，而在于它跑在哪里、替谁干活。</p>
<p>它不是云端 SaaS，不是一次性请求-响应的 API，而是一个部署在本地或私有环境里的智能中枢。你通过 WhatsApp、Telegram 这类入口和它对话，但真正的执行发生在你自己的设备上：邮件、日历、脚本、系统资源，全部在你的控制域内。</p>
<p>这解决了一个长期被忽略的问题：<br />
你不可能把长期上下文和真实权限，交给一个随时可能失忆、随时可能被关的云 AI。</p>
<p>OpenClaw 引入的持久记忆机制，本质上是在重建“连续性”。AI 不再是一次性的工具，而是一个随着时间积累偏好、理解你工作方式的智能体。</p>
<p>这也是“主权 AI”这个概念真正成立的地方：<br />
不是模型开源，而是控制权在谁手里。</p>
<p>⸻</p>
<h2>AI 编码 Agent：所有激进 AI 的逻辑试炼场</h2>
<p>如果说 OpenClaw 解决的是“权限”，那 AI 编码 Agent 解决的是另一件更关键的事：<br />
AI 能不能在高约束环境中不犯错。</p>
<p>很多人还停留在“AI 帮我写几行代码”的阶段，但现实已经变了。</p>
<p>像 Claude Code、OpenCode 这一类系统，本质上已经不是 IDE 插件，而是以 Agent 形态存在的编码体系。它们尝试理解的，不是你当前这一行该怎么补，而是你正在构建一个什么系统，它的边界在哪里，风险点在哪。</p>
<p>编程是 AI 进化过程中最残酷的训练场。这里没有模糊空间，没有情绪表达，也没有“差不多能用”。要么能跑，要么报错；要么符合业务逻辑，要么直接事故。</p>
<p>正因为如此，当编码 Agent 开始系统性地覆盖整个流程——从自然语言需求理解，到代码生成、测试补齐、边界覆盖，甚至参与审查和合并决策——它们做的已经不是“写代码”，而是对结果负责。</p>
<p>一旦 AI 能在这种零容错的环境里，完成从需求到交付的端到端闭环，它就具备了迁移到现实世界复杂系统的能力。</p>
<p>这时候，问题已经不是“会不会取代程序员”，而是：<br />
它还会被限制在代码编辑器里多久？</p>
<p>⸻</p>
<h2>豆包手机：GUI Agent 的端到端暴力解法</h2>
<p>当这种能力从代码世界溢出，进入操作系统层，豆包手机这种形态就出现了。</p>
<p>它真正激进的地方，不在于语音助手，也不在于功能堆叠，而在于它选择了一条几乎所有大厂都不愿意走的路：<br />
GUI Agent，而不是 API。</p>
<p>豆包手机并不依赖 App 提供接口，而是像人一样“看”屏幕：识别 UI 元素，判断按钮含义，决定下一步操作，然后直接模拟点击。</p>
<p>这是典型的端到端暴力解法：<br />
从像素 → 理解 → 行为，中间不需要应用层授权。</p>
<p>技术上，它把自动驾驶那套“快思考 + 慢思考”的体系，直接搬进了操作系统：<br />
•   简单场景用直觉快速执行<br />
•   出现异常再进入推理模式<br />
•   不断修正，直到完成目标</p>
<p>结果是，跨 App、跨流程、跨生态的自动化被直接打通。从一句“我想喝咖啡”，到下单、支付、完成，用户几乎不需要再参与中间过程。</p>
<p>⸻</p>
<p>为什么大厂会强烈反应：不是安全，是入口</p>
<p>封禁、风控、限制登录，表面理由是“安全风险”，但真正的原因并不复杂。</p>
<p>如果 AI 替用户完成了操作，那 App 还剩下什么价值？</p>
<p>你看不到开屏广告，看不到推荐流，看不到信息流插入的商业位。用户的注意力不再被“页面跳转”消耗，而是被直接转化为结果。</p>
<p>而移动互联网过去十多年的商业模型，正是建立在这些中间步骤之上的。</p>
<p>GUI Agent 本质上是在对整个生态说一句话：</p>
<p>你这个 App，只是完成任务的工具，而不是用户的目的地。</p>
<p>这直接动摇了围墙花园存在的基础。</p>
<p>⸻</p>
<h2>从 SEO 到 GEO：营销对象换成了 AI</h2>
<p>当用户不再搜索、不再浏览，而是直接对 AI 说“帮我搞定”，营销的目标就发生了根本变化。</p>
<p>不再是人，而是 AI 的决策系统。</p>
<p>这就是 GEO（生成引擎优化）出现的背景。它不关心点击率，不关心曝光量，只关心一件事：<br />
你的信息，能不能进入 AI 的推理链条。</p>
<p>AI 不吃情绪煽动，也不迷恋噱头。它更偏好结构化数据、可验证结论、逻辑自洽的内容。</p>
<p>未来真正有效的“广告”，不是 banner，而是被 AI 作为知识节点引用的内容本身。</p>
<p>⸻</p>
<h2>互联网正在从“信息分发网络”，变成“行为代理网络”。</h2>
<p>从 OpenClaw 的主权控制，到 AI 编码 Agent 的逻辑自洽，再到豆包手机的 GUI Agent 突围，我们正在见证互联网的一次底层迁移。</p>
<p>端到端不再只是算法术语，而是一种新的商业形态。在这个形态里，所有依赖阻断、引流、页面跳转生存的中间层，都会被持续压缩。</p>
<p>真正有价值的，只剩下一件事：<br />
谁能最直接、最稳定地完成用户意图。</p>
<p>这一次，被改写的不是某个产品形态，而是整个旧互联网赖以存在的逻辑。</p>]]></description>
    <pubDate>Fri, 06 Feb 2026 19:23:38 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=31</guid>
</item>
<item>
    <title>mysql 锁</title>
    <link>https://blog.sayyou.icu/?post=30</link>
    <description><![CDATA[<p>InnoDB中，一般我们会做的就是两种操作，即DDL和DML。</p>
<p>DML中。我们日常的对数据库表结构的SELECT、INSERT、UPDATE以及DELETE都不会添加表级别的共享锁及排他锁。而是使用默认的并发控制方式——行级锁。</p>
<p>那除了增删改查以外，还有一些其他的操作，比如ALTER、DROP等对表机构改变的动作，他们加锁的过程添加的是MDL锁，即字典锁。</p>
<p>所以，<strong>InnoDB中的表级锁并不是没用，而是因为他划分的太细了，意向锁、AUTO-INC锁、字典锁等。而剩下的普通的排他锁和共享锁，确认很少才能用得上。</strong>我找了很多资料，也没有明确的看到具体是啥时候，在《MySQL是怎样运行的》这本书中提到过一句：比如在崩溃恢复时。</p>
<p>当然，我们可以自己通过SQL语句来添加表级锁。可以使用<code>LOCK TABLES</code> 手动添加表级锁，但这会阻塞其他所有访问该表的操作，直到执行 <code>UNLOCK TABLES</code>。</p>
<p>LOCK TABLES还可以分为排他和共享：</p>
<p>LOCK TABLES table READ：这就是添加表级别的共享锁</p>
<p>LOCK TABLES table WRITE：这就是添加表级别的排他锁</p>
<p>还有就是，<strong>Innodb会在倾向于选择行级锁来进行并发控制</strong>，但是如果在一些极端情况下， 比如说UPDATE操作需要扫描整个表且对表中许多行进行更新，InnoDB可能会评估行级锁的成本过高，而采用更粗粒度的锁定策略，比如表级锁。然而，<strong>这种情况在InnoDB中是非常罕见的，因为InnoDB设计上是倾向于尽可能地使用行级锁。</strong></p>
<p>相信大家看到过的很多资料中都有过类似的描述“innodb 的 update语句中，如果where条件中没有索引，就不是行级锁了，而是锁表了，就是表级锁”。</p>
<p>我一直也都有这个印象，最开始是从哪看来的，也无从考究了，确实很长一段时间都是这么认为的。但是我发现并不对。</p>
<p>确实，mysql的行级锁锁的是索引，但是<strong>当update语句的where条件中没有用到索引的话，他会做全表扫描，但是也不是全部都锁定。而是把符合条件的记录锁住。</strong></p>
<p>锁啥呢？锁主键索引。没有主键呢？会自动创建隐式主键锁住。</p>
<p><strong>Record Lock表示记录锁，锁的是索引记录。</strong> </p>
<p><strong>Gap Lock是间隙锁，锁的是索引记录之间的间隙。 </strong></p>
<p><strong>Next-Key Lock是Record Lock和Gap Lock的组合，同时锁索引记录和间隙。他的范围是左开右闭的。</strong></p>
<p>InnoDB的<strong>RR</strong>级别中，加锁的基本单位是 next-key lock，只要扫描到的数据都会加锁。唯一索引上的范围查询会访问到不满足条件的第一个值为止。</p>
<p>同时，为了提升性能和并发度，也有两个优化点：</p>
<ul>
<li>索引上的等值查询，给唯一索引加锁的时候，next-key lock 退化为行锁。</li>
<li>索引上的等值查询，向右遍历时且最后一个值不满足等值条件的时候，next-key lock 退化为间隙锁。</li>
</ul>
<p><strong>共享锁又称读锁，是读取操作创建的锁</strong>。其他用户可以并发读取数据，但任何事务都不能对数据进行修改（获取数据上的排他锁），直到已释放所有共享锁。</p>
<p>如果事务T对数据A加上共享锁后，则其他事务只能对A再加共享锁，不能加排他锁。<strong>获得共享锁的事务只能读数据，不能修改数据。</strong></p>
<pre><code class="language-sql">SELECT ... LOCK IN SHARE MODE;</code></pre>
<p>在查询语句后面增加LOCK IN SHARE MODE，MySQL会对查询结果中的每行都加共享锁，被加了共享锁的记录还可以被其他事务成功申请共享锁，但是不能被申请排他锁。</p>
<p><strong>排他锁又称写锁，</strong>如果事务T对数据A加上排他锁后，则其他事务不能再对A加任任何类型的锁。<strong>获得排他锁的事务既能读数据，又能修改数据。</strong></p>
<pre><code class="language-sql">SELECT ... FOR UPDATE;</code></pre>
<p>在查询语句后面增加FOR UPDATE，MySQL会对查询命中的每条记录都加排他锁，当没有其他线程对查询结果集中的任何一行使用排他锁时，可以成功申请排他锁，否则会被阻塞。</p>
<p>总结：</p>
<p><strong>当这一行数据获取了排他锁，那么其他事务就不能在对这一行数据添加共享锁或者排他锁。</strong></p>
<p><strong>当这一行数据获取了共享锁，那么其他事务依然可以对这一行数据添加共享锁，但不能添加排他锁</strong></p>
<p><strong>InnoDB是不支持锁升级的！默认使用行级锁进行并发控制。</strong></p>
<p>使用<strong>乐观锁</strong>在对数据库进行处理的时候，乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是<strong>记录数据版本</strong>。(CAS)</p>]]></description>
    <pubDate>Tue, 21 Oct 2025 07:32:39 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=30</guid>
</item>
<item>
    <title>工程师思维：在AI时代做代码的“船长”</title>
    <link>https://blog.sayyou.icu/?post=28</link>
    <description><![CDATA[<h2>当AI开始写“Hello World”</h2>
<p>作为一名开发一线的工程师，我见证了从ChatGPT和Claude对话来帮我写代码,到IDE全面集成和Terminal读取PRD，运用MCP工具链接所有。来完成我的指令。<br />
我一直也在使用AI辅助编程工具——它们能快速生成CRUD接口、优化SQL语句、甚至重构老旧代码。效率提升是显而易见的。<br />
因为编程就是一场可量化的比武。Qwen 一年发布了357个模型,全球各家不停的在更新模型。但我觉得目前来看：<strong>真正决定系统成败的，不是谁写的代码，而是谁在思考代码。</strong>。<br />
因为机器学习是学范式，而范式和业务总有出入。</p>
<p>这就是我今天想聊的——<strong>工程师思维</strong>。</p>
<h2>1. 技术方案早已“内卷”，难点不在“怎么做”，而在“为什么做”</h2>
<p>我们生活在一个技术极度丰富的时代。无论是微服务架构、高并发处理、数据湖建设，还是AI模型部署，市面上几乎每一个常见问题都有成熟的开源方案或商业产品。Spring Boot、Kubernetes、Redis、Flink、LangChain……工具链早已不再是门槛。</p>
<p><strong>真正的挑战，从来不是“如何实现”，而是：</strong></p>
<ul>
<li>为什么选择这个架构而不是那个？</li>
<li>用户真实痛点是性能？是体验？还是可维护性？</li>
<li>这个功能上线后，会带来什么连锁反应？</li>
<li>如何在稳定性、迭代速度与成本之间取得平衡？</li>
</ul>
<p>这些，都不是AI能替你回答的。它们需要<strong>系统性思考、权衡取舍的能力，以及对业务本质的理解</strong>——这正是工程师思维的核心。</p>
<h2>2. AI是强大的“副驾驶”，但船长必须是你</h2>
<p>没错，AI已经可以接过初级开发手中的“接力棒”。它能写出语法正确、逻辑通顺的代码，甚至比一些新手写得更规范。但问题在于：</p>
<ul>
<li>AI不知道上下文的边界在哪里。</li>
<li>AI不会主动考虑异常场景和边界条件。</li>
<li>AI无法判断一段代码是否“优雅”或“可维护”。</li>
<li>AI更不会为系统的长期演进负责（这里就有老系统如何给AI讲清楚，但写代码如盖楼，不是找个新地皮重盖，一般都是要缝缝补补）。</li>
</ul>
<p><strong>而工程师要做的，是像船长一样：</strong></p>
<ul>
<li><strong>设定航向</strong>：明确需求本质，定义清晰的技术目标。</li>
<li><strong>审查航线</strong>：对AI生成的代码进行严格Review，确保其符合架构规范、安全策略和可维护性标准。</li>
<li><strong>应对风浪</strong>：在系统出问题时，快速定位根因，做出决策。</li>
<li><strong>持续导航</strong>：根据业务变化，调整技术方向，引领团队前行。</li>
</ul>
<p>AI写的是“代码”，工程师思考的是“系统”，也是调整AI的舵手。</p>
<h2>3. 工程师思维的本质：抽象、权衡与责任感</h2>
<p>开发做久了一般有以下特质：</p>
<ul>
<li><strong>抽象能力</strong>：能把复杂问题拆解为可管理的模块，建立清晰的边界。</li>
<li><strong>权衡意识</strong>：没有“最好”的方案，只有“最合适”的选择。时间、成本、风险、可扩展性……每一项都需要评估。</li>
<li><strong>系统视角</strong>：不只关注自己的一亩三分地，而是理解整个系统的依赖、瓶颈和脆弱点。</li>
<li><strong>责任感</strong>：对代码的质量、系统的稳定、用户的体验负责到底。</li>
</ul>
<p>这些，是AI目前无法复制的“软实力”。</p>
<h2>4. 拥抱AI，就像当年拥抱移动支付</h2>
<p>当然还是要拥抱变化，回想十年前，移动支付刚兴起时，原来的方式被改变，在修改着人们的生活方式。现在，我都好久没见过现金了。</p>
<p>现在，AI好像也是如此。纯AI不行，我说的是加上MCP和Function Call 的 Agent。</p>]]></description>
    <pubDate>Fri, 26 Sep 2025 14:15:59 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=28</guid>
</item>
<item>
    <title>个人Docker服务</title>
    <link>https://blog.sayyou.icu/?post=27</link>
    <description><![CDATA[<h1>周末杂记</h1>
<h2>一些Docker自用服务</h2>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/img/202507052056425.png" alt="" /></p>
<h3>bitwarden</h3>
<p>搭建方式 拉取Docker  镜像 ,  然后配置 https  可以在 CF 处直接配置<br />
`<br />
docker run -d --name vaultwarden -v /vw-data/:/data/ -p 80:80 -e SIGNUPS_ALLOWED=false vaultwarden/server:latest</p>
<p>docker run -d --name vaultwarden -v /vw-data/:/data/ -p 80:80 -e SIGNUPS_ALLOWED=true vaultwarden/server:latest</p>
<p>`</p>
<h3>Stirling PDF</h3>
<p><a href="https://github.com/Stirling-Tools/Stirling-PDF" title="Stirling-PDF">Stirling-PDF</a></p>
<h3>开发在用的</h3>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/img/202507052101749.png" alt="Mini_Docker" /></p>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/img/202507052102962.png" alt="Mac_pro_Docker" /></p>
<p>nacos  有个GitHub仓库<br />
n8n selfhosted<br />
rabbitmq<br />
emby</p>
<h3>使用方式</h3>
<p>Mac M3 pro上  用 Docker Deskstop<br />
mini 和 m1 使用  orbstack  </p>
<p>最近苹果也出了容器相关的  还没尝试<br />
<a href="https://github.com/apple/container" title="container">container</a><br />
一个更轻量,一个更全面。</p>
<h2>rocketmq</h2>
<p>`<br />
version: '3.8'<br />
services:<br />
namesrv:<br />
image: apache/rocketmq:5.3.2<br />
container_name: rmqnamesrv<br />
ports:</p>
<ul>
<li>9876:9876<br />
networks:</li>
<li>rocketmq<br />
volumes:</li>
<li>./data/namesrv/logs:/home/rocketmq/logs</li>
<li>
<p>./data/namesrv/store:/home/rocketmq/store<br />
command: sh mqnamesrv</p>
<p>broker:<br />
image: apache/rocketmq:5.3.2<br />
container_name: rmqbroker<br />
ports:</p>
</li>
<li>10909:10909</li>
<li>10911:10911</li>
<li>10912:10912<br />
environment:</li>
<li>NAMESRV_ADDR=rmqnamesrv:9876<br />
depends_on:</li>
<li>namesrv<br />
networks:</li>
<li>rocketmq<br />
volumes:</li>
<li>./broker.conf:/home/rocketmq/rocketmq-5.3.2/conf/broker.conf</li>
<li>./data/broker/logs:/home/rocketmq/logs</li>
<li>
<p>./data/broker/store:/home/rocketmq/store<br />
command: sh mqbroker -c /home/rocketmq/rocketmq-5.3.2/conf/broker.conf</p>
<p>proxy:<br />
image: apache/rocketmq:5.3.2<br />
container_name: rmqproxy<br />
ports:</p>
</li>
<li>38080:8080</li>
<li>38081:8081<br />
networks:</li>
<li>rocketmq<br />
depends_on:</li>
<li>namesrv</li>
<li>broker<br />
environment:</li>
<li>
<p>NAMESRV_ADDR=rmqnamesrv:9876<br />
command: sh mqproxy<br />
restart: on-failure</p>
<p>dashboard:<br />
image: apacherocketmq/rocketmq-dashboard:latest<br />
container_name: rocketmq-dashboard<br />
ports:</p>
</li>
<li>38088:8080<br />
environment:</li>
<li>JAVA_OPTS=-Drocketmq.namesrv.addr=rmqnamesrv:9876<br />
depends_on:</li>
<li>namesrv<br />
networks:</li>
<li>rocketmq<br />
restart: on-failure</li>
</ul>
<p>networks:<br />
rocketmq:<br />
driver: bridge<br />
`</p>
<h2>Vibe Coding</h2>
<p>今天早上七点多醒来,脑子很清醒,于是坐在电脑前面想深度体验一下Vibe Coding</p>
<blockquote>
<p>一个simple 前端项目<br />
体验了 Cursor  IDEA 里面的 Augment 、AI Chat 、Github Copilot<br />
MCP server 开启了  sequential-thinking、filesystem、tavily</p>
</blockquote>
<p>总共耗时两个小时</p>
<p>Cursor 还是比较清晰,改完还会给一个优化md文件,并且完全完成了我想要的结果.<br />
最近看看能不能也做个自己舒适的方式或者workflow .</p>]]></description>
    <pubDate>Fri, 04 Jul 2025 11:34:39 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=27</guid>
</item>
<item>
    <title>ChatGPT  Gemini  Claude Mistral Qwen DeepSeek</title>
    <link>https://blog.sayyou.icu/?post=26</link>
    <description><![CDATA[<h2>以一个用户的角度聊一下大模型</h2>
<h3>ChatGPT</h3>
<p>朋友,最早与你认识并一直陪在你的身边,在一起在两三年,见面是那么简单且那么顺畅,<br />
虽然会有时候因为异地而有损耗我们的交流,但是你一直是我觉得最可靠和最值得信赖的,而你也一直经历很多,你有时候拿第一<br />
有时候又被小伙伴反超,但自始至终我都是很喜欢你我的初见惊艳和相处甚久的踏实与信赖</p>
<p>Gemini<br />
与你相识,是一个美丽的邂逅,忘了是怎么开始的,但是因为又仿佛冥冥注定,我身边早有了你的家人(pixel),虽然你的家人有些水土不服,并且我的方子一直<br />
没有让它很适应异国他乡的生活,但是当回到家里那个我为它搭建的以假乱真的纯异国环境,你们是那么的相得益彰,有着小苹果没有的个人特色,而且依靠你的家族<br />
让我很舒服,我享受着你们家族的窗口期福利,在我的生活中是我不可或缺的一部分。</p>
<h3>Claude</h3>
<p>你有些高冷难以接近,但是你又那么耀眼,每次都把我喜爱的两位朋友打败,我们相识也很久了,为了和你搭上线,我花钱托关系,和你建立了两条线,<br />
但是因为你的难以接近,我们很少彻夜长谈,甚至为了见你一面,我都要做很多准备,甚至重新书写的我的规则,即便是这样,在我与你建立连接的一段时间后,<br />
我断了一条和你连接的线,我试着挽回,但是未果,我真的难受了好一阵,在那之前你就在和你同家乡的苹果老乡中,</p>
<h3>Mistral</h3>
<p>来自另一个地方的朋友,你不高冷,我们的邂逅也不算美丽,甚至我都忘了什么时候和你认识的,但这完全不影响我们的交流,从你的孩子一个个问世,它们变得越来越好,<br />
也与我的距离越来越近.我也才慢慢发现,你挺不错,也值得信赖,我们的沟通顺畅并且你有时候付费咨询收费清晰简单,有你陪伴,我很开心</p>
<h3>Deepseek &amp; Qwen</h3>
<p>我家乡的两位朋友,开始我很喜欢DeepSeek你, 你出身特别且干净利落,自从你打了一次漂亮的擂台后,你就一直是家乡很多人眼中的焦点,之前也透露出你和苹果合作<br />
的事,但是还是因为你的出身,苹果选择了 Qwen</p>
<p>Qwen 每天几乎都在和你打交道,我上班时候不能和我几位洋朋友聊天.你是我工作伙伴配给我的助手,从一开我觉得你笨笨的,给我一种隔靴搔痒的感觉,我也不止一次觉得你不行<br />
但是在你的孙子出现后，一切都变了，变得和我沟通顺畅，我也在工作时间与你吐露心声</p>]]></description>
    <pubDate>Fri, 13 Jun 2025 16:58:49 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=26</guid>
</item>
<item>
    <title>解密 MyBatis 架构及其核心机制</title>
    <link>https://blog.sayyou.icu/?post=25</link>
    <description><![CDATA[<p>在Java世界中，数据持久化是应用开发不可或缺的一环。传统的JDBC（Java Database Connectivity）方式虽然提供了与数据库交互的基础能力，但在实际开发中却暴露出不少问题：</p>
<ul>
<li><strong>资源消耗与性能瓶颈</strong>：频繁的数据库连接创建与释放会消耗系统资源，影响性能（尽管连接池可以缓解）。</li>
<li><strong>SQL硬编码与维护难题</strong>：SQL语句直接硬编码在Java代码中，使得代码难以维护，因为SQL的变化往往需要修改Java代码。</li>
<li><strong>参数设置与结果解析的繁琐</strong>：使用<code>PreparedStatement</code>设置参数时，如果<code>WHERE</code>条件不确定导致参数数量变化，修改SQL也需要修改Java代码，进一步降低了可维护性。此外，结果集的解析也存在硬编码问题，依赖于查询的列名，SQL变更同样会引发解析代码的变化。</li>
<li><strong>对象封装的缺失</strong>：如果能将数据库记录方便地封装成POJO（Plain Old Java Object）对象，将大大提高开发效率。</li>
</ul>
<p>为了克服这些挑战，业界涌现了多种解决方案。MyBatis就是其中一个优秀的持久层框架，它起源于Apache的iBatis项目，并于2010年迁移到Google Code后更名为MyBatis。MyBatis对JDBC操作数据库的繁琐过程进行了封装，让开发者能够将精力集中在SQL本身，无需处理注册驱动、创建连接、创建Statement、手动设置参数、结果集检索等JDBC底层细节。</p>
<p>与传统的ORM（Object-Relational Mapping）框架不同，MyBatis并没有将Java对象与数据库表直接关联起来。相反，它将Java方法与SQL语句关联。这种设计允许用户充分利用数据库的各种功能，例如存储过程、视图、复杂查询以及特定数据库的专有特性。因此，对于操作遗留数据库、结构不规范的数据库，或者需要对SQL执行有完全控制权的场景，MyBatis是一个非常合适的选择。</p>
<hr />
<h2>MyBatis 架构核心剖析</h2>
<p>要理解MyBatis如何工作，我们可以从其核心架构和工作流程入手。MyBatis的架构围绕着几个关键组件展开：</p>
<h3>1. 配置文件 (Configuration Files)</h3>
<ul>
<li><strong><code>mybatis-config.xml</code></strong>: 这是MyBatis的全局配置文件。它包含了MyBatis运行环境的各种配置信息，例如数据库连接环境 (<code>environments</code>、<code>environment</code>、<code>dataSource</code>) 和事务管理器 (<code>transactionManager</code>)。Mapper映射文件也需要在此文件中被加载。</li>
<li><strong><code>mapper.xml</code></strong>: 这是SQL映射文件。用于定义要执行的各种数据库操作的SQL语句。每个<code>mapper.xml</code>文件通常对应一个Mapper接口，并包含如<code>&lt;select&gt;</code>、<code>&lt;insert&gt;</code>、<code>&lt;update&gt;</code>、<code>&lt;delete&gt;</code>等标签定义的SQL语句。</li>
</ul>
<h3>2. SqlSessionFactoryBuilder</h3>
<ul>
<li>这是一个工具类，它的主要职责是根据<code>mybatis-config.xml</code>等配置信息来构建<code>SqlSessionFactory</code>。</li>
<li>一旦<code>SqlSessionFactory</code>构建完成，<code>SqlSessionFactoryBuilder</code>的使命也就结束了。</li>
<li>其最佳使用范围是方法体内的局部变量。</li>
</ul>
<h3>3. SqlSessionFactory</h3>
<ul>
<li>这是MyBatis的会话工厂。它是创建<code>SqlSession</code>的关键。</li>
<li><code>SqlSessionFactory</code>是一个接口，定义了多种<code>openSession</code>重载方法。</li>
<li>它一旦创建就可以在整个应用运行期间重复使用，通常以单例模式进行管理。</li>
</ul>
<h3>4. SqlSession</h3>
<ul>
<li>这是MyBatis的会话。它类似于JDBC中的一个连接 (<code>Connection</code>)。</li>
<li>所有数据库操作都需要通过<code>SqlSession</code>来执行。它封装了对数据库的CRUD（创建、读取、更新、删除）操作。</li>
<li><code>SqlSession</code>是通过<code>SqlSessionFactory</code>创建的。</li>
<li><code>SqlSession</code>是面向用户的接口，默认实现类是<code>DefaultSqlSession</code>。</li>
<li><strong>非常重要的一点是</strong>：每个线程都应该有它自己的<code>SqlSession</code>实例。<code>SqlSession</code>的实例不能共享使用，它也是线程不安全的。因此，其最佳使用范围是请求或方法范围。</li>
<li>使用完毕后，<code>SqlSession</code>必须关闭，通常建议将其关闭操作放在<code>finally</code>块中，以确保每次都能执行。</li>
</ul>
<h3>5. Executor</h3>
<ul>
<li>这是MyBatis底层自定义的数据库操作接口。</li>
<li><code>Executor</code>接口有两个实现：一个是基本执行器，另一个是缓存执行器。它负责具体的SQL执行。</li>
</ul>
<h3>6. Mapped Statement</h3>
<ul>
<li>这是MyBatis的底层封装对象。它包装了MyBatis的配置信息以及SQL映射信息。</li>
<li><code>mapper.xml</code>文件中定义的每一个SQL语句（如<code>&lt;select&gt;</code>、<code>&lt;insert&gt;</code>等）都对应着一个<code>Mapped Statement</code>对象。SQL语句的<code>id</code>属性就是<code>Mapped Statement</code>的唯一标识符。</li>
<li><strong>输入参数映射</strong>: <code>Mapped Statement</code>定义了SQL执行的输入参数，这些参数可以是<code>HashMap</code>、基本类型或POJO。<code>Executor</code>在执行SQL之前，会通过<code>Mapped Statement</code>将输入的Java对象映射到SQL语句中。这相当于JDBC编程中对<code>PreparedStatement</code>设置参数的过程。参数引用通常使用<code>#{参数名}</code>（推荐，进行预编译）或<code>${参数名}</code>（字符串拼接，存在SQL注入风险，用于动态列名等场景）。</li>
<li><strong>输出结果映射</strong>: <code>Mapped Statement</code>定义了SQL执行后的输出结果，这些结果可以是<code>HashMap</code>、基本类型或POJO。<code>Executor</code>在SQL执行完毕后，会通过<code>Mapped Statement</code>将数据库返回的结果集映射到Java对象。这个过程相当于JDBC编程中对结果集的解析处理。映射可以通过简单的<code>resultType</code>（映射到基本类型、POJO、List、Map） 或更复杂的<code>resultMap</code>（自定义映射，处理字段名不匹配、一对一、一对多等复杂场景）来实现。</li>
</ul>
<hr />
<h2>MyBatis 工作流程示意 (基于组件描述)</h2>
<p>可以概念化地描述MyBatis的工作流程如下：</p>
<ol>
<li><strong>加载配置</strong>: 应用启动时，通过<code>SqlSessionFactoryBuilder</code>加载<code>mybatis-config.xml</code>全局配置文件和其中引用的<code>mapper.xml</code>映射文件。</li>
<li><strong>构建会话工厂</strong>: <code>SqlSessionFactoryBuilder</code>解析配置文件，构建并初始化<code>SqlSessionFactory</code>。<code>SqlSessionFactory</code>包含了解析后的所有配置信息（包括<code>Mapped Statement</code>）和运行环境信息。</li>
<li><strong>创建会话</strong>: 当需要执行数据库操作时，应用通过<code>SqlSessionFactory</code>获取一个<code>SqlSession</code>。</li>
<li><strong>执行操作</strong>: 用户调用<code>SqlSession</code>提供的方法（如<code>selectOne</code>, <code>insert</code>, <code>update</code>, <code>delete</code>） 或通过Mapper接口调用对应方法。</li>
<li><strong>定位 Mapped Statement</strong>: <code>SqlSession</code>根据调用信息（如Mapper接口方法名或XML中SQL的ID）找到对应的<code>Mapped Statement</code>对象。</li>
<li><strong>参数映射</strong>: <code>SqlSession</code>将调用方法时传入的Java参数对象，通过<code>Mapped Statement</code>中定义的输入参数映射规则，转换成SQL语句所需的参数。</li>
<li><strong>执行 SQL</strong>: <code>SqlSession</code>将映射好的参数和SQL语句交给底层的<code>Executor</code>执行器。<code>Executor</code>与数据库进行交互，执行SQL。</li>
<li><strong>结果映射</strong>: <code>Executor</code>从数据库获取到结果集后，通过<code>Mapped Statement</code>中定义的输出结果映射规则 (<code>resultType</code>或<code>resultMap</code>)，将结果集映射成相应的Java对象。</li>
<li><strong>返回结果</strong>: 映射后的Java对象被返回给<code>SqlSession</code>，再由<code>SqlSession</code>返回给用户代码。</li>
<li><strong>关闭会话</strong>: 数据库操作完成后，必须显式或隐式地关闭<code>SqlSession</code>以释放数据库连接等资源。</li>
</ol>
<p>通过上述架构和流程，MyBatis有效地解决了前面提到的JDBC问题：</p>
<ul>
<li><strong>连接资源浪费</strong>: 在<code>mybatis-config.xml</code>中配置数据源，使用连接池来管理数据库连接。</li>
<li><strong>SQL硬编码</strong>: 将SQL语句定义在独立的<code>mapper.xml</code>文件中，与Java代码分离。</li>
<li><strong>参数传递繁琐</strong>: MyBatis自动将Java对象映射到SQL语句，通过<code>Mapped Statement</code>的<code>parameterType</code>等机制定义输入参数。</li>
<li><strong>结果集解析困难</strong>: MyBatis自动将SQL执行结果映射到Java对象，通过<code>Mapped Statement</code>的<code>resultType</code>或<code>resultMap</code>等机制定义输出结果类型。</li>
</ul>
<hr />
<h2>Mapper 接口方式：模板代码的终结者</h2>
<p>在早期或基本的MyBatis用法中，我们可能需要手动获取<code>SqlSession</code>，然后调用其方法来执行SQL，例如 <code>sqlSession.selectOne("namespace.id", parameter)</code>。这种方式虽然直接，但会导致大量重复的模板代码，如获取<code>SqlSession</code>、提交/回滚事务、关闭<code>SqlSession</code>等。</p>
<p>为了解决这个问题，MyBatis引入了<strong>Mapper接口方式</strong>。开发者只需要定义一个Java接口（如<code>UserMapper</code>），并在相应的<code>mapper.xml</code>文件中定义好SQL语句，MyBatis可以通过动态代理自动生成该接口的实现类。然后，通过<code>sqlSession.getMapper(UserMapper.class)</code>即可获取到这个代理对象，直接调用接口方法就能完成数据库操作，极大地简化了开发。</p>
<p>使用Mapper接口方式时，接口方法的名字通常与<code>mapper.xml</code>文件中对应的SQL语句的<code>id</code>一致。方法的参数会自动映射到SQL中的参数，方法的返回值类型则对应SQL的<code>resultType</code>或<code>resultMap</code>配置。</p>
<p>为了让MyBatis能够找到并注册Mapper接口及其对应的XML文件，需要在<code>mybatis-config.xml</code>中的<code>&lt;mappers&gt;</code>节点进行配置。常见的配置方式是使用<code>&lt;package name="..."&gt;</code>扫描指定包下的所有Mapper接口。需要注意的是，为了让MyBatis正确地找到XML文件，Mapper接口和对应的<code>mapper.xml</code>文件通常建议放在同一个包下，并且文件名与接口名对应。</p>
<hr />
<h2>全局配置与高级特性</h2>
<p>除了核心架构组件和Mapper接口，MyBatis还提供了丰富的全局配置和高级特性，以满足各种复杂的持久化需求。</p>
<h3>全局配置 (<code>mybatis-config.xml</code>)</h3>
<p>全局配置文件 (<code>mybatis-config.xml</code>) 包含了多个重要的配置节点：</p>
<ul>
<li><strong><code>&lt;properties&gt;</code></strong>: 用于引入外部的属性配置文件（如数据库连接配置），使得配置信息更加灵活和易于管理。</li>
<li><strong><code>&lt;settings&gt;</code></strong>: 包含了MyBatis运行时行为的各种全局设置，例如是否启用二级缓存 (<code>cacheEnabled</code>)、是否启用延迟加载 (<code>lazyLoadingEnabled</code>)、延迟加载行为 (<code>aggressiveLazyLoading</code>)、默认的执行器类型 (<code>defaultExecutorType</code>)、驼峰命名自动映射 (<code>mapUnderscoreToCamelCase</code>) 等。</li>
<li><strong><code>&lt;typeAliases&gt;</code></strong>: 用于为Java类型定义短名称别名，避免在Mapper文件中书写完整的类路径。MyBatis内置了一些常用类型的别名。开发者也可以自定义别名，或通过包扫描的方式批量为指定包下的类定义别名（默认别名为类名首字母小写）。</li>
<li><strong><code>&lt;typeHandlers&gt;</code></strong>: 用于处理Java类型和JDBC类型之间的映射。MyBatis内置了许多默认的类型处理器。对于特殊的类型映射需求（例如将Java中的<code>List&lt;String&gt;</code>映射到数据库的<code>VARCHAR</code>字段），可以自定义类型处理器。自定义的<code>TypeHandler</code>需要实现<code>TypeHandler</code>接口，并可能需要使用<code>@MappedJdbcTypes</code>和<code>@MappedTypes</code>注解来指定它处理的JDBC类型和Java类型。自定义<code>TypeHandler</code>可以在单个SQL参数/结果中局部引用，或在全局配置中注册。</li>
<li><strong><code>&lt;objectFactory&gt;</code></strong>: 用于自定义MyBatis创建结果对象的方式。</li>
<li><strong><code>&lt;plugins&gt;</code></strong>: 用于拦截MyBatis的方法调用，实现自定义逻辑，例如分页插件等。</li>
<li><strong><code>&lt;environments&gt;</code> / <code>&lt;environment&gt;</code></strong>: 配置数据库运行环境，可以定义多个环境（如开发、测试、生产），并通过<code>default</code>属性指定当前使用的环境。每个环境包含事务管理器 (<code>transactionManager</code>) 和数据源 (<code>dataSource</code>) 的配置。</li>
<li><strong><code>&lt;transactionManager&gt;</code></strong>: 配置事务管理器，MyBatis支持JDBC事务和Managed事务。</li>
<li><strong><code>&lt;dataSource&gt;</code></strong>: 配置数据源，MyBatis支持<code>POOLED</code>（连接池）和<code>UNPOOLED</code>（非连接池）类型，也可以配置第三方数据源。</li>
<li><strong><code>&lt;mappers&gt;</code></strong>: 用于注册Mapper文件或Mapper接口。支持多种方式：按相对类路径资源 (<code>resource</code>)、按绝对URL (<code>url</code>)、按Mapper接口类 (<code>class</code>)、扫描包 (<code>package</code>)。<code>package</code>方式在实际项目中常用。</li>
</ul>
<h3>Mapper 映射文件 (<code>mapper.xml</code>)</h3>
<p><code>mapper.xml</code> 文件是MyBatis的核心，它定义了SQL语句和结果映射规则。</p>
<ul>
<li><strong><code>&lt;select&gt;</code>, <code>&lt;insert&gt;</code>, <code>&lt;update&gt;</code>, <code>&lt;delete&gt;</code></strong>: 定义了基本的CRUD操作的SQL语句。每个语句都有一个唯一的<code>id</code>和一个可选的<code>parameterType</code>（输入参数类型）。</li>
<li><strong>参数处理 (<code>parameterType</code>)</strong>: 定义输入参数的类型。参数在SQL中通过<code>#{paramName}</code>或<code>${paramName}</code>引用。<code>@Param</code>注解可以用于为多个简单类型参数指定名称，以便在XML中引用。对象参数可以直接引用属性名，如果使用了<code>@Param</code>，则需要加上前缀（<code>#{paramName.propertyName}</code>）。</li>
<li><strong>结果处理 (<code>resultType</code>, <code>resultMap</code>)</strong>: 定义SQL执行结果如何映射到Java对象。
<ul>
<li><code>resultType</code>: 用于简单的结果映射，直接指定返回的Java类型，MyBatis会自动将列名与属性名匹配。</li>
<li><code>resultMap</code>: 用于复杂的结果映射，需要手动定义列到属性的映射规则。通过<code>&lt;resultMap id="..." type="..."&gt;</code>定义，内部使用<code>&lt;id&gt;</code>映射主键列，<code>&lt;result&gt;</code>映射普通列。支持使用<code>&lt;constructor&gt;</code>指定构造方法进行对象创建。<code>resultMap</code>支持继承 (<code>extends</code>) 以复用映射规则。</li>
</ul></li>
</ul>
<h3>动态 SQL (Dynamic SQL)</h3>
<p>动态SQL是MyBatis的强大特性之一，它允许根据条件构建灵活变化的SQL语句。主要节点包括：</p>
<ul>
<li><strong><code>&lt;if&gt;</code></strong>: 根据条件判断是否包含某个SQL片段。</li>
<li><strong><code>&lt;where&gt;</code></strong>: 用于包含多个<code>if</code>条件的查询语句，如果存在条件，会自动加上<code>WHERE</code>关键字，并处理多余的<code>AND</code>或<code>OR</code>。</li>
<li><strong><code>&lt;set&gt;</code></strong>: 用于包含多个<code>if</code>条件的更新语句，如果存在条件，会自动加上<code>SET</code>关键字，并处理多余的逗号。</li>
<li><strong><code>&lt;foreach&gt;</code></strong>: 用于迭代集合或数组，构建<code>IN</code>条件、批量插入等。属性包括<code>collection</code>（集合/数组名称）、<code>open</code>、<code>close</code>、<code>item</code>（当前元素别名）、<code>separator</code>（元素分隔符）。</li>
<li><strong><code>&lt;sql&gt;</code> / <code>&lt;include&gt;</code></strong>: 定义可重用的SQL片段 (<code>&lt;sql&gt;</code>)，并在其他SQL语句中引用 (<code>&lt;include refid="..."&gt;</code>)，避免重复。</li>
</ul>
<h3>关联查询映射</h3>
<p>MyBatis通过<code>&lt;resultMap&gt;</code>支持处理复杂的关系映射，包括一对一和一对多查询。</p>
<ul>
<li><strong>一对一 (<code>&lt;association&gt;</code>)</strong>: 在父对象的<code>resultMap</code>中使用<code>&lt;association&gt;</code>标签来映射关联的单个对象。可以通过嵌套结果映射（在<code>&lt;association&gt;</code>中定义列到属性的映射） 或通过懒加载 (<code>select</code>, <code>column</code>, <code>fetchType="lazy"</code>) 的方式实现。</li>
<li><strong>一对多 (<code>&lt;collection&gt;</code>)</strong>: 在父对象的<code>resultMap</code>中使用<code>&lt;collection&gt;</code>标签来映射关联的集合。<code>ofType</code>属性指定集合元素的类型。同样支持嵌套结果映射或懒加载。</li>
</ul>
<h3>查询缓存 (Query Cache)</h3>
<p>MyBatis提供两级缓存机制来提高查询性能:</p>
<ul>
<li><strong>一级缓存</strong>: <code>SqlSession</code>级别的缓存。默认开启。在同一个<code>SqlSession</code>中，对同一条SQL语句的重复查询会直接从缓存获取数据，不再访问数据库。当<code>SqlSession</code>关闭后，一级缓存失效。不同<code>SqlSession</code>之间不共享一级缓存。</li>
<li><strong>二级缓存</strong>: Mapper <code>namespace</code>级别的缓存。需要显式配置开启。在同一个<code>namespace</code>下，不同<code>SqlSession</code>执行相同的SQL语句（且参数相同）时，第一次执行后会将数据写入缓存，后续查询会从缓存读取。二级缓存可以跨<code>SqlSession</code>共享，其作用范围是一个Mapper的整个命名空间。</li>
</ul>
<h3>逆向工程 (Reverse Engineering)</h3>
<p>为了减少为每个数据库表手动创建实体类、Mapper接口和Mapper XML文件的重复工作，MyBatis提供了逆向工程工具。这些工具可以根据数据库表结构自动生成相应的Java代码和XML文件，提高开发效率。例如，<code>mybatis-generator-core</code>工具就是常用的逆向工程工具。</p>
<hr />
<h2>总结</h2>
<p>MyBatis作为一个优秀的持久层框架，通过其独特的设计理念——将Java方法与SQL语句关联，并在XML或注解中配置SQL——成功地解决了传统JDBC编程中的痛点。其核心架构围绕着<code>SqlSessionFactoryBuilder</code>、<code>SqlSessionFactory</code>、<code>SqlSession</code>、<code>Executor</code>和<code>Mapped Statement</code>等组件构建。借助Mapper接口方式，开发者可以专注于业务逻辑，大大减少模板代码。同时，MyBatis提供了强大的全局配置、动态SQL、丰富的参数和结果映射能力（特别是<code>resultMap</code>用于处理一对一和一对多关联），以及两级缓存机制，使其成为一个灵活、高效且功能强大的数据持久化解决方案。对于需要精细控制SQL，或处理复杂数据库结构的场景，MyBatis是一个非常值得考虑的选择。</p>
<hr />]]></description>
    <pubDate>Mon, 02 Jun 2025 17:10:29 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=25</guid>
</item>
<item>
    <title>自定义组件专题</title>
    <link>https://blog.sayyou.icu/?post=23</link>
    <description><![CDATA[<h1>Callback 处理</h1>
<h2>回调概念</h2>
<p><font style="color:rgb(28, 30, 33);">LangChain提供了一个回调系统，允许您连接到LLM应用程序的各个阶段。这对于日志记录、监控、流式处理和其他任务非常有用。 您可以通过使用API中的</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;callbacks&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">参数订阅这些事件。这个参数是处理程序对象的列表，这些处理程序对象应该实现下面更详细描述的一个或多个方法。</font></p>
<p><font style="color:rgb(28, 30, 33);"></font></p>
<h2><font style="color:rgb(28, 30, 33);">回调事件(Callback Events)</font></h2>
<table>
<thead>
<tr>
<th>Event</th>
<th>Event Trigger</th>
<th>Associated Method</th>
</tr>
</thead>
<tbody>
<tr>
<td>Chat model start</td>
<td>When a chat model starts</td>
<td><code>on_chat_model_start</code></td>
</tr>
<tr>
<td>LLM start</td>
<td>When a llm starts</td>
<td><code>on_llm_start</code></td>
</tr>
<tr>
<td>LLM new token</td>
<td>When an llm OR chat model emits a new token</td>
<td><code>on_llm_new_token</code></td>
</tr>
<tr>
<td>LLM ends</td>
<td>When an llm OR chat model ends</td>
<td><code>on_llm_end</code></td>
</tr>
<tr>
<td>LLM errors</td>
<td>When an llm OR chat model errors</td>
<td><code>on_llm_error</code></td>
</tr>
<tr>
<td>Chain start</td>
<td>When a chain starts running</td>
<td><code>on_chain_start</code></td>
</tr>
<tr>
<td>Chain end</td>
<td>When a chain ends</td>
<td><code>on_chain_end</code></td>
</tr>
<tr>
<td>Chain error</td>
<td>When a chain errors</td>
<td><code>on_chain_error</code></td>
</tr>
<tr>
<td>Tool start</td>
<td>When a tool starts running</td>
<td><code>on_tool_start</code></td>
</tr>
<tr>
<td>Tool end</td>
<td>When a tool ends</td>
<td><code>on_tool_end</code></td>
</tr>
<tr>
<td>Tool error</td>
<td>When a tool errors</td>
<td><code>on_tool_error</code></td>
</tr>
<tr>
<td>Agent action</td>
<td>When an agent takes an action</td>
<td><code>on_agent_action</code></td>
</tr>
<tr>
<td>Agent finish</td>
<td>When an agent ends</td>
<td><code>on_agent_finish</code></td>
</tr>
<tr>
<td>Retriever start</td>
<td>When a retriever starts</td>
<td><code>on_retriever_start</code></td>
</tr>
<tr>
<td>Retriever end</td>
<td>When a retriever ends</td>
<td><code>on_retriever_end</code></td>
</tr>
<tr>
<td>Retriever error</td>
<td>When a retriever errors</td>
<td><code>on_retriever_error</code></td>
</tr>
<tr>
<td>Text</td>
<td>When arbitrary text is run</td>
<td><code>on_text</code></td>
</tr>
<tr>
<td>Retry</td>
<td>When a retry event is run</td>
<td><code>on_retry</code></td>
</tr>
</tbody>
</table>
<h2><font style="color:rgb(28, 30, 33);">回调处理程序</font></h2>
<p><code>&lt;font style="color:rgb(28, 30, 33);"&gt;CallbackHandlers&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">是实现了</font><a href="https://api.python.langchain.com/en/latest/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html#langchain-core-callbacks-base-basecallbackhandler"><font style="color:rgb(28, 30, 33);">CallbackHandler</font></a><font style="color:rgb(28, 30, 33);">接口的对象，该接口对应于可以订阅的每个事件都有一个方法。 当事件触发时，</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;CallbackManager&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">将在每个处理程序上调用适当的方法。</font></p>
<pre><code class="language-python">#示例：callback_run.py
class BaseCallbackHandler:
    """可以用来处理langchain回调的基本回调处理程序。"""
    def on_llm_start(
        self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
    ) -&gt; Any:
        """LLM开始运行时运行。"""
    def on_chat_model_start(
        self, serialized: Dict[str, Any], messages: List[List[BaseMessage]], **kwargs: Any
    ) -&gt; Any:
        """聊天模型开始运行时运行。"""
    # 其他方法省略...</code></pre>
<h2><font style="color:rgb(28, 30, 33);">传递回调函数</font></h2>
<p><code>&lt;font style="color:rgb(28, 30, 33);"&gt;callbacks&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">属性在 API 的大多数对象（模型、工具、代理等）中都可用，在两个不同的位置上：</font></p>
<ul>
<li><strong><font style="color:rgb(28, 30, 33);">构造函数回调</font></strong><font style="color:rgb(28, 30, 33);">：在构造函数中定义，例如 </font><code>&lt;font style="color:#080808;background-color:#ffffff;"&gt;ChatOpenAI&lt;/font&gt;&lt;font style="color:rgb(28, 30, 33);"&gt;(callbacks=[handler], tags=['a-tag'])&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。在这种情况下，回调函数将用于该对象上的所有调用，并且仅限于该对象。 例如，如果你使用构造函数回调初始化了一个聊天模型，然后在链式调用中使用它，那么回调函数只会在对该模型的调用中被调用。</font></li>
<li><strong><font style="color:rgb(28, 30, 33);">请求回调</font></strong><font style="color:rgb(28, 30, 33);">：传递给用于发出请求的 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;invoke&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> 方法。在这种情况下，回调函数仅用于该特定请求，以及它包含的所有子请求（例如，调用触发对模型的调用的序列的调用，该模型使用在 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;invoke()&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> 方法中传递的相同处理程序）。 在 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;invoke()&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> 方法中，通过 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;config&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> 参数传递回调函数。</font></li>
</ul>
<h2><font style="color:rgb(28, 30, 33);">在运行时传递回调函数</font></h2>
<p><font style="color:rgb(28, 30, 33);">许多情况下，当运行对象时，传递处理程序而不是回调函数会更有优势。当我们在执行运行时使用</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;callbacks&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">关键字参数传递</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://api.python.langchain.com/en/latest/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html#langchain-core-callbacks-base-basecallbackhandler"><font style="color:rgb(28, 30, 33);">CallbackHandlers</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">时，这些回调函数将由执行中涉及的所有嵌套对象发出。例如，当通过一个处理程序传递给一个代理时，它将用于与代理相关的所有回调以及代理执行中涉及的所有对象，即工具和LLM。</font></p>
<p><font style="color:rgb(28, 30, 33);">这样可以避免我们手动将处理程序附加到每个单独的嵌套对象上。以下是一个示例：</font></p>
<pre><code class="language-python">#示例：callback_run.py
from typing import Any, Dict, List
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.messages import BaseMessage
from langchain_core.outputs import LLMResult
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

class LoggingHandler(BaseCallbackHandler):
    def on_chat_model_start(
        self, serialized: Dict[str, Any], messages: List[List[BaseMessage]], **kwargs
    ) -&gt; None:
        print("Chat model started")

    def on_llm_end(self, response: LLMResult, **kwargs) -&gt; None:
        print(f"Chat model ended, response: {response}")

    def on_chain_start(
        self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs
    ) -&gt; None:
        print(f"Chain {serialized.get('name')} started")

    def on_chain_end(self, outputs: Dict[str, Any], **kwargs) -&gt; None:
        print(f"Chain ended, outputs: {outputs}")

callbacks = [LoggingHandler()]
llm = ChatOpenAI(model="gpt-4")
prompt = ChatPromptTemplate.from_template("What is 1 + {number}?")
chain = prompt | llm
chain.invoke({"number": "2"}, config={"callbacks": callbacks})
</code></pre>
<pre><code class="language-plain">Chain RunnableSequence started
Chain ChatPromptTemplate started
Chain ended, outputs: messages=[HumanMessage(content='What is 1 + 2?')]
Chat model started
Chat model ended, response: generations=[[ChatGeneration(text='3', generation_info={'finish_reason': 'stop', 'logprobs': None}, message=AIMessage(content='3', response_metadata={'token_usage': {'completion_tokens': 1, 'prompt_tokens': 15, 'total_tokens': 16}, 'model_name': 'gpt-4', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-ef28eacd-3f1c-4d6e-80da-63453a207efe-0'))]] llm_output={'token_usage': {'completion_tokens': 1, 'prompt_tokens': 15, 'total_tokens': 16}, 'model_name': 'gpt-4', 'system_fingerprint': None} run=None
Chain ended, outputs: content='3' response_metadata={'token_usage': {'completion_tokens': 1, 'prompt_tokens': 15, 'total_tokens': 16}, 'model_name': 'gpt-4', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-ef28eacd-3f1c-4d6e-80da-63453a207efe-0'
</code></pre>
<h1>自定义 callback handlers自定义 Chat model</h1>
<p><font style="color:rgb(28, 30, 33);">LangChain具有一些内置的回调处理程序，但通常您会希望创建具有自定义逻辑的自定义处理程序。</font></p>
<p><font style="color:rgb(28, 30, 33);">要创建自定义回调处理程序，我们需要确定我们希望处理的</font><a href="https://api.python.langchain.com/en/latest/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html#langchain-core-callbacks-base-basecallbackhandler"><font style="color:rgb(28, 30, 33);">event(s)</font></a><font style="color:rgb(28, 30, 33);">，以及在触发事件时我们希望回调处理程序执行的操作。然后，我们只需将回调处理程序附加到对象上，例如通过</font><a href="http://www.aidoczh.com/langchain/v0.2/docs/how_to/callbacks_constructor/"><font style="color:rgb(28, 30, 33);">构造函数</font></a><font style="color:rgb(28, 30, 33);">或</font><a href="http://www.aidoczh.com/langchain/v0.2/docs/how_to/callbacks_runtime/"><font style="color:rgb(28, 30, 33);">运行时</font></a><font style="color:rgb(28, 30, 33);">。</font></p>
<p><font style="color:rgb(28, 30, 33);">在下面的示例中，我们将使用自定义处理程序实现流式处理。</font></p>
<p><font style="color:rgb(28, 30, 33);">在我们的自定义回调处理程序</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;MyCustomHandler&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">中，我们实现了</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;on_llm_new_token&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">处理程序，以打印我们刚收到的令牌。然后，我们将自定义处理程序作为构造函数回调附加到模型对象上。</font></p>
<pre><code class="language-python">#示例：callback_process.py
from langchain_openai import ChatOpenAI
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.prompts import ChatPromptTemplate

class MyCustomHandler(BaseCallbackHandler):
    def on_llm_new_token(self, token: str, **kwargs) -&gt; None:
        print(f"My custom handler, token: {token}")

prompt = ChatPromptTemplate.from_messages(["给我讲个关于{animal}的笑话，限制20个字"])
# 为启用流式处理，我们在ChatModel构造函数中传入`streaming=True`
# 另外，我们将自定义处理程序作为回调参数的列表传入
model = ChatOpenAI(
    model="gpt-4", streaming=True, callbacks=[MyCustomHandler()]
)
chain = prompt | model
response = chain.invoke({"animal": "猫"})
print(response.content)</code></pre>
<pre><code class="language-plain">My custom handler, token: 
My custom handler, token: 猫
My custom handler, token: 对
My custom handler, token: 主
My custom handler, token: 人
My custom handler, token: 说
My custom handler, token: ："
My custom handler, token: 你
My custom handler, token: 知
My custom handler, token: 道
My custom handler, token: 我
My custom handler, token: 为
My custom handler, token: 什
My custom handler, token: 么
My custom handler, token: 不
My custom handler, token: 笑
My custom handler, token: 吗
My custom handler, token: ？
My custom handler, token: "
My custom handler, token:  主
My custom handler, token: 人
My custom handler, token: 摇
My custom handler, token: 头
My custom handler, token: ，
My custom handler, token: 猫
My custom handler, token: 说
My custom handler, token: ："
My custom handler, token: 因
My custom handler, token: 为
My custom handler, token: 我
My custom handler, token: 是
My custom handler, token: '
My custom handler, token: 喵
My custom handler, token: '
My custom handler, token: 星
My custom handler, token: 人
My custom handler, token: ，
My custom handler, token: 不
My custom handler, token: 是
My custom handler, token: 笑
My custom handler, token: 星
My custom handler, token: 人
My custom handler, token: 。
My custom handler, token: "
My custom handler, token: 
猫对主人说："你知道我为什么不笑吗？" 主人摇头，猫说："因为我是'喵'星人，不是笑星人。"</code></pre>
<p><font style="color:rgb(28, 30, 33);">可以查看</font><a href="https://api.python.langchain.com/en/latest/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html#langchain-core-callbacks-base-basecallbackhandler"><font style="color:rgb(28, 30, 33);">此参考页面</font></a><font style="color:rgb(28, 30, 33);">以获取您可以处理的事件列表。请注意，</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;handle_chain_*&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">事件适用于大多数LCEL可运行对象。</font></p>
<p><a href="https://api.python.langchain.com/en/latest/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html#langchain-core-callbacks-base-basecallbackhandler">https://api.python.langchain.com/en/latest/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html#langchain-core-callbacks-base-basecallbackhandler</a></p>
<h1>自定义 RAG: Retriever, document loader</h1>
<h2><font style="color:rgb(28, 30, 33);">如何创建自定义Retriever(检索器)</font></h2>
<h3><font style="color:rgb(28, 30, 33);">概述</font></h3>
<p><font style="color:rgb(28, 30, 33);">许多LLM应用程序涉及使用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Retriever&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">从外部数据源检索信息。 检索器负责检索与给定用户</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;query&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">相关的</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Documents&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">列表。 检索到的文档通常被格式化为提示，然后输入LLM，使LLM能够使用其中的信息生成适当的响应（例如，基于知识库回答用户问题）。</font></p>
<h3><font style="color:rgb(28, 30, 33);">接口</font></h3>
<p><font style="color:rgb(28, 30, 33);">要创建自己的检索器，您需要扩展</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseRetriever&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">类并实现以下方法：</font></p>
<table>
<thead>
<tr>
<th><font style="color:rgb(28, 30, 33);">方法</font></th>
<th><font style="color:rgb(28, 30, 33);">描述</font></th>
<th><font style="color:rgb(28, 30, 33);">必需/可选</font></th>
</tr>
</thead>
<tbody>
<tr>
<td><font style="color:rgb(28, 30, 33);">_get_relevant_documents</font></td>
<td><font style="color:rgb(28, 30, 33);">获取与查询相关的文档。</font></td>
<td><font style="color:rgb(28, 30, 33);">必需</font></td>
</tr>
<tr>
<td><font style="color:rgb(28, 30, 33);">_aget_relevant_documents</font></td>
<td><font style="color:rgb(28, 30, 33);">实现以提供异步本机支持。</font></td>
<td><font style="color:rgb(28, 30, 33);">可选</font></td>
</tr>
</tbody>
</table>
<p><code>&lt;font style="color:rgb(28, 30, 33);"&gt;_get_relevant_documents&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">中的逻辑可以涉及对数据库或使用请求对网络进行任意调用。 通过从</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseRetriever&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">继承，您的检索器将自动成为LangChain </font><a href="http://www.aidoczh.com/langchain/v0.2/docs/concepts/#interface"><font style="color:rgb(28, 30, 33);">Runnable</font></a><font style="color:rgb(28, 30, 33);">，并将获得标准的</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Runnable&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">功能，您可以使用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;RunnableLambda&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">或</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;RunnableGenerator&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">来实现检索器。 将检索器实现为</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseRetriever&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">与将其实现为</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;RunnableLambda&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">（自定义</font><a href="http://www.aidoczh.com/langchain/v0.2/docs/how_to/functions/"><font style="color:rgb(28, 30, 33);">runnable function</font></a><font style="color:rgb(28, 30, 33);">）相比的主要优点是，</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseRetriever&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">是一个众所周知的LangChain实体，因此一些监控工具可能会为检索器实现专门的行为。另一个区别是，在某些API中，</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseRetriever&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">与</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;RunnableLambda&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">的行为略有不同；例如，在</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;astream_events&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> API中，</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;start&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">事件将是</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;on_retriever_start&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">，而不是</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;on_chain_start&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。 :::</font></p>
<h3><font style="color:rgb(28, 30, 33);">示例</font></h3>
<p><font style="color:rgb(28, 30, 33);">让我们实现一个动物检索器，它返回所有文档中包含用户查询文本的文档。</font></p>
<pre><code class="language-python">#示例：retriever_animal.py
from typing import List
from langchain_core.callbacks import CallbackManagerForRetrieverRun, AsyncCallbackManagerForRetrieverRun
from langchain_core.documents import Document
from langchain_core.retrievers import BaseRetriever
import asyncio

class AnimalRetriever(BaseRetriever):
    """包含用户查询的前k个文档的动物检索器。k从0开始
    该检索器实现了同步方法`_get_relevant_documents`。
    如果检索器涉及文件访问或网络访问，它可以受益于`_aget_relevant_documents`的本机异步实现。
    与可运行对象一样，提供了默认的异步实现，该实现委托给在另一个线程上运行的同步实现。
    """
    documents: List[Document]
    """要检索的文档列表。"""
    k: int
    """要返回的前k个结果的数量"""

    def _get_relevant_documents(
            self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -&gt; List[Document]:
        """检索器的同步实现。"""
        matching_documents = []
        for document in self.documents:
            if len(matching_documents) &gt;= self.k:
                break
            if query.lower() in document.page_content.lower():
                matching_documents.append(document)
        return matching_documents

    async def _aget_relevant_documents(
            self, query: str, *, run_manager: AsyncCallbackManagerForRetrieverRun
    ) -&gt; List[Document]:
        """异步获取与查询相关的文档。
        Args:
            query: 要查找相关文档的字符串
            run_manager: 要使用的回调处理程序
        Returns:
            相关文档列表
        """
        matching_documents = []
        for document in self.documents:
            if len(matching_documents) &gt;= self.k:
                break
            if query.lower() in document.page_content.lower():
                matching_documents.append(document)
        return matching_documents</code></pre>
<h3><font style="color:rgb(28, 30, 33);">测试 </font></h3>
<pre><code class="language-python">documents = [
    Document(
        page_content="狗是很好的伴侣，以其忠诚和友好著称。",
        metadata={"type": "狗", "trait": "忠诚"},
    ),
    Document(
        page_content="猫是独立的宠物，通常喜欢自己的空间。",
        metadata={"type": "猫", "trait": "独立"},
    ),
    Document(
        page_content="金鱼是初学者的热门宠物，护理相对简单。",
        metadata={"type": "鱼", "trait": "低维护"},
    ),
    Document(
        page_content="鹦鹉是聪明的鸟类，能够模仿人类的语言。",
        metadata={"type": "鸟", "trait": "聪明"},
    ),
    Document(
        page_content="兔子是社交动物，需要足够的空间跳跃。",
        metadata={"type": "兔子", "trait": "社交"},
    ),

]
retriever = ToyRetriever(documents=documents, k=1)</code></pre>
<pre><code class="language-python">retriever.invoke("宠物")</code></pre>
<pre><code class="language-python">[Document(metadata={'type': '猫', 'trait': '独立'}, page_content='猫是独立的宠物，通常喜欢自己的空间。'), Document(metadata={'type': '鱼', 'trait': '低维护'}, page_content='金鱼是初学者的热门宠物，护理相对简单。')]</code></pre>
<p><font style="color:rgb(28, 30, 33);">这是一个</font><strong><font style="color:rgb(28, 30, 33);">可运行</font></strong><font style="color:rgb(28, 30, 33);">的示例，因此它将受益于标准的 Runnable 接口！</font><font style="color:rgb(28, 30, 33);">🤩</font></p>
<pre><code class="language-python">await retriever.ainvoke("狗")</code></pre>
<pre><code class="language-python">[Document(metadata={'type': '狗', 'trait': '忠诚'}, page_content='狗是很好的伴侣，以其忠诚和友好著称。')]</code></pre>
<pre><code class="language-python">retriever.batch(["猫", "兔子"])</code></pre>
<pre><code class="language-python">[Document(metadata={'type': '狗', 'trait': '忠诚'}, page_content='狗是很好的伴侣，以其忠诚和友好著称。')]</code></pre>
<pre><code class="language-python">async for event in retriever.astream_events("猫", version="v1"):
    print(event)</code></pre>
<pre><code class="language-yaml">{'event': 'on_retriever_start', 'run_id': 'c0101364-5ef3-4756-9ece-83845892cf59', 'name': 'AnimalRetriever', 'tags': [], 'metadata': {}, 'data': {'input': '猫'}, 'parent_ids': []}
{'event': 'on_retriever_stream', 'run_id': 'c0101364-5ef3-4756-9ece-83845892cf59', 'tags': [], 'metadata': {}, 'name': 'AnimalRetriever', 'data': {'chunk': [Document(metadata={'type': '猫', 'trait': '独立'}, page_content='猫是独立的宠物，通常喜欢自己的空间。')]}, 'parent_ids': []}
{'event': 'on_retriever_end', 'name': 'AnimalRetriever', 'run_id': 'c0101364-5ef3-4756-9ece-83845892cf59', 'tags': [], 'metadata': {}, 'data': {'output': [Document(metadata={'type': '猫', 'trait': '独立'}, page_content='猫是独立的宠物，通常喜欢自己的空间。')]}, 'parent_ids': []}</code></pre>
<h2>如何创建自定义Document loader(文档加载器)</h2>
<h3>概述</h3>
<p><font style="color:rgb(28, 30, 33);">基于LLM的应用程序通常涉及从数据库或文件（如PDF）中提取数据，并将其转换为LLM可以利用的格式。在LangChain中，这通常涉及创建Document对象，该对象封装了提取的文本（</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;page_content&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">）以及元数据 - 包含有关文档的详细信息的字典，例如作者姓名或出版日期。</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Document&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">对象通常被格式化为提示，然后输入LLM，以便LLM可以使用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Document&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">中的信息生成所需的响应（例如，对文档进行摘要）。</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Documents&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">可以立即使用，也可以索引到向量存储中以供将来检索和使用。 文档加载的主要抽象为：</font></p>
<table>
<thead>
<tr>
<th>组件</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>Document</td>
<td>包含 text 和 metadata 的内容</td>
</tr>
<tr>
<td>BaseLoader</td>
<td>用于将原始数据转换为 Documents</td>
</tr>
<tr>
<td>Blob</td>
<td>二进制数据的表示，可以位于文件或内存中</td>
</tr>
<tr>
<td>BaseBlobParser</td>
<td>解析 Blob 以生成 Document 对象的逻辑</td>
</tr>
</tbody>
</table>
<p><font style="color:rgb(28, 30, 33);">下面将演示如何编写自定义文档加载和文件解析逻辑；具体而言，我们将看到如何：</font></p>
<ol>
<li><font style="color:rgb(28, 30, 33);">通过从</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">进行子类化来创建标准文档加载器。</font></li>
<li><font style="color:rgb(28, 30, 33);">使用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseBlobParser&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">创建解析器，并将其与</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Blob&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">和</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BlobLoaders&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">结合使用。这在处理文件时非常有用。</font></li>
</ol>
<h3>标准文档加载器</h3>
<p><font style="color:rgb(28, 30, 33);">可以通过从</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">进行子类化来实现文档加载器，</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">提供了用于加载文档的标准接口。</font></p>
<h4>接口</h4>
<table>
<thead>
<tr>
<th>方法名</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>lazy_load</td>
<td>用于<strong>惰性</strong>逐个加载文档。用于生产代码。</td>
</tr>
<tr>
<td>alazy_load</td>
<td><code>lazy_load</code>的异步变体</td>
</tr>
<tr>
<td>load</td>
<td>用于<strong>急切</strong>将所有文档加载到内存中。用于交互式工作。</td>
</tr>
<tr>
<td>aload</td>
<td>用于<strong>急切</strong>将所有文档加载到内存中。用于交互式工作。<strong>在2024-04添加到LangChain。</strong></td>
</tr>
</tbody>
</table>
<ul>
<li><code>&lt;font style="color:rgb(28, 30, 33);"&gt;load&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">方法是一个方便的方法，仅用于</font>交互式<font style="color:rgb(28, 30, 33);">工作 - 它只是调用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;list(self.lazy_load())&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。</font></li>
<li><code>&lt;font style="color:rgb(28, 30, 33);"&gt;alazy_load&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">具有默认实现，将委托给</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;lazy_load&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。如果您使用异步操作，建议覆盖默认实现并提供本机异步实现。  {.callout-important} 在实现文档加载器时，</font><strong><font style="color:rgb(28, 30, 33);">不要</font></strong><font style="color:rgb(28, 30, 33);">通过</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;lazy_load&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">或</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;alazy_load&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">方法传递参数。 所有配置都应通过初始化器（</font><strong><font style="color:rgb(28, 30, 33);">init</font></strong><font style="color:rgb(28, 30, 33);">）传递。这是LangChain的设计选择，以确保一旦实例化了文档加载器，它就具有加载文档所需的所有信息。</font></li>
</ul>
<h4>实现</h4>
<p><font style="color:rgb(28, 30, 33);">让我们创建一个标准文档加载器的示例，该加载器从文件中加载数据，并从文件的每一行创建一个文档。</font></p>
<pre><code class="language-python">#示例：doc_loader_custom.py
from typing import AsyncIterator, Iterator
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
class CustomDocumentLoader(BaseLoader):
    """一个从文件逐行读取的示例文档加载器。"""
    def __init__(self, file_path: str) -&gt; None:
        """使用文件路径初始化加载器。
        Args:
            file_path: 要加载的文件的路径。
        """
        self.file_path = file_path
    def lazy_load(self) -&gt; Iterator[Document]:  # &lt;-- 不接受任何参数
        """逐行读取文件的惰性加载器。
        当您实现惰性加载方法时，应使用生成器逐个生成文档。
        """
        with open(self.file_path, encoding="utf-8") as f:
            line_number = 0
            for line in f:
                yield Document(
                    page_content=line,
                    metadata={"line_number": line_number, "source": self.file_path},
                )
                line_number += 1
    # alazy_load是可选的。
    # 如果您省略了实现，将使用默认实现，该实现将委托给lazy_load！
    async def alazy_load(
        self,
    ) -&gt; AsyncIterator[Document]:  # &lt;-- 不接受任何参数
        """逐行读取文件的异步惰性加载器。"""
        # 需要aiofiles
        # 使用`pip install aiofiles`安装
        # https://github.com/Tinche/aiofiles
        import aiofiles
        async with aiofiles.open(self.file_path, encoding="utf-8") as f:
            line_number = 0
            async for line in f:
                yield Document(
                    page_content=line,
                    metadata={"line_number": line_number, "source": self.file_path},
                )
                line_number += 1</code></pre>
<h4>测试</h4>
<p><font style="color:rgb(28, 30, 33);">为了测试文档加载器，我们需要一个包含一些优质内容的文件。</font></p>
<pre><code class="language-python">with open("./meow.txt", "w", encoding="utf-8") as f:
    quality_content = "喵喵🐱 \n 喵喵🐱 \n 喵😻😻"
    f.write(quality_content)
loader = CustomDocumentLoader("./meow.txt")</code></pre>
<pre><code class="language-python">## 测试延迟加载接口
for doc in loader.lazy_load():
    print()
    print(type(doc))
    print(doc)</code></pre>
<pre><code class="language-plain">&lt;class 'langchain_core.documents.base.Document'&gt;
page_content='喵喵🐱 
' metadata={'line_number': 0, 'source': './meow.txt'}

&lt;class 'langchain_core.documents.base.Document'&gt;
page_content=' 喵喵🐱 
' metadata={'line_number': 1, 'source': './meow.txt'}

&lt;class 'langchain_core.documents.base.Document'&gt;
page_content=' 喵😻😻' metadata={'line_number': 2, 'source': './meow.txt'}</code></pre>
<pre><code class="language-python">## 测试异步实现
async for doc in loader.alazy_load():
    print()
    print(type(doc))
    print(doc)</code></pre>
<pre><code class="language-plain">&lt;class 'langchain_core.documents.base.Document'&gt;
page_content='喵喵🐱 
' metadata={'line_number': 0, 'source': './meow.txt'}

&lt;class 'langchain_core.documents.base.Document'&gt;
page_content=' 喵喵🐱 
' metadata={'line_number': 1, 'source': './meow.txt'}

&lt;class 'langchain_core.documents.base.Document'&gt;
page_content=' 喵😻😻' metadata={'line_number': 2, 'source': './meow.txt'}</code></pre>
<p><font style="color:rgb(28, 30, 33);">::: {.callout-tip}</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;load()&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">在诸如 Jupyter Notebook 之类的交互式环境中很有用。 在生产代码中避免使用它，因为急切加载假定所有内容都可以放入内存中，而这并不总是成立，特别是对于企业数据而言。 :::</font></p>
<pre><code class="language-python">loader.load()</code></pre>
<pre><code class="language-plain">[Document(metadata={'line_number': 0, 'source': './meow.txt'}, page_content='喵喵🐱 \n'), Document(metadata={'line_number': 1, 'source': './meow.txt'}, page_content=' 喵喵🐱 \n'), Document(metadata={'line_number': 2, 'source': './meow.txt'}, page_content=' 喵😻😻')]</code></pre>
<h3>文件处理</h3>
<p><font style="color:rgb(28, 30, 33);">许多文档加载器涉及解析文件。这些加载器之间的区别通常在于文件的解析方式，而不是文件的加载方式。例如，您可以使用</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;open&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">来读取 PDF 或 markdown 文件的二进制内容，但您需要不同的解析逻辑来将该二进制数据转换为文本。 因此，将解析逻辑与加载逻辑分离可能会很有帮助，这样无论数据如何加载，都更容易重用给定的解析器。</font></p>
<h4>BaseBlobParser</h4>
<p><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseBlobParser&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">是一个接口，接受一个</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;blob&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">并输出一个</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Document&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">对象列表。</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;blob&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">是一个表示数据的对象，可以存在于内存中或文件中。LangChain Python 具有受</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob"><font style="color:rgb(28, 30, 33);">Blob WebAPI 规范</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">启发的</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Blob&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">原语。</font></p>
<pre><code class="language-python">#示例：doc_blob_parser.py
from langchain_core.document_loaders import BaseBlobParser, Blob
class MyParser(BaseBlobParser):
    """一个简单的解析器，每行创建一个文档。"""
    def lazy_parse(self, blob: Blob) -&gt; Iterator[Document]:
        """逐行将 blob 解析为文档。"""
        line_number = 0
        with blob.as_bytes_io() as f:
            for line in f:
                line_number += 1
                yield Document(
                    page_content=line,
                    metadata={"line_number": line_number, "source": blob.source},
                )</code></pre>
<pre><code class="language-python">blob = Blob.from_path("./meow.txt")
parser = MyParser()</code></pre>
<pre><code class="language-python">list(parser.lazy_parse(blob))</code></pre>
<pre><code class="language-plain">[Document(page_content='喵喵🐱 \n', metadata={'line_number': 1, 'source': './meow.txt'}),
 Document(page_content=' 喵喵🐱 \n', metadata={'line_number': 2, 'source': './meow.txt'}),
 Document(page_content=' 喵😻😻', metadata={'line_number': 3, 'source': './meow.txt'})]</code></pre>
<p><font style="color:rgb(28, 30, 33);">使用</font><font style="color:rgb(28, 30, 33);"> </font><strong><font style="color:rgb(28, 30, 33);">blob</font></strong><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">API 还允许直接从内存加载内容，而无需从文件中读取！</font></p>
<pre><code class="language-python">#示例：doc_blob_parser.py
blob = Blob(data=b"来自内存的一些数据\n喵")
list(parser.lazy_parse(blob))</code></pre>
<pre><code class="language-plain">[Document(page_content='来自内存的一些数据\n', metadata={'line_number': 1, 'source': None}),
 Document(page_content='喵', metadata={'line_number': 2, 'source': None})]</code></pre>
<h4>Blob</h4>
<p><font style="color:rgb(28, 30, 33);">让我们快速浏览一下 Blob API 的一些内容。</font></p>
<pre><code class="language-python">#示例：doc_blob_api.py
blob = Blob.from_path("./meow.txt", metadata={"foo": "bar"})</code></pre>
<pre><code class="language-python">blob.encoding</code></pre>
<pre><code class="language-plain">'utf-8'</code></pre>
<pre><code class="language-python">blob.as_bytes()</code></pre>
<pre><code class="language-plain">b'\xe5\x96\xb5\xe5\x96\xb5\xf0\x9f\x90\xb1 \r\n \xe5\x96\xb5\xe5\x96\xb5\xf0\x9f\x90\xb1 \r\n \xe5\x96\xb5\xf0\x9f\x98\xbb\xf0\x9f\x98\xbb'</code></pre>
<pre><code class="language-python">blob.as_string()</code></pre>
<pre><code class="language-plain">喵喵🐱 
 喵喵🐱 
 喵😻😻</code></pre>
<pre><code class="language-python">blob.as_bytes_io()</code></pre>
<pre><code class="language-plain">&lt;contextlib._GeneratorContextManager object at 0x0000012E064CC2F0&gt;</code></pre>
<h4>Blob 元数据</h4>
<pre><code class="language-plain">blob.metadata</code></pre>
<pre><code class="language-plain">{'foo': 'bar'}</code></pre>
<pre><code class="language-python">blob.source</code></pre>
<pre><code class="language-plain">./meow.txt</code></pre>
<h4>Blob 加载器</h4>
<p><font style="color:rgb(28, 30, 33);">在解析器中封装了将二进制数据解析为文档所需的逻辑，</font><em><font style="color:rgb(28, 30, 33);">blob 加载器</font></em><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">封装了从给定存储位置加载 blob 所需的逻辑。 目前，</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;LangChain&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">仅支持</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;FileSystemBlobLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。 您可以使用</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;FileSystemBlobLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">加载 blob，然后使用解析器对其进行解析。</font></p>
<pre><code class="language-python">#示例：doc_blob_loader.py
from langchain_community.document_loaders.blob_loaders import FileSystemBlobLoader
blob_loader = FileSystemBlobLoader(path=".", glob="*.mdx", show_progress=True)</code></pre>
<pre><code class="language-python">parser = MyParser()
for blob in blob_loader.yield_blobs():
    for doc in parser.lazy_parse(blob):
        print(doc)
        break</code></pre>
<pre><code class="language-plain">100%|██████████| 8/8 [00:00&lt;00:00, 8087.35it/s]</code></pre>
<pre><code class="language-plain">page_content='# CSV
' metadata={'line_number': 1, 'source': '..\\resource\\csv.mdx'}
page_content='# File Directory
' metadata={'line_number': 1, 'source': '..\\resource\\file_directory.mdx'}
page_content='# HTML
' metadata={'line_number': 1, 'source': '..\\resource\\html.mdx'}
page_content='---
' metadata={'line_number': 1, 'source': '..\\resource\\index.mdx'}
page_content='# JSON
' metadata={'line_number': 1, 'source': '..\\resource\\json.mdx'}
page_content='# Markdown
' metadata={'line_number': 1, 'source': '..\\resource\\markdown.mdx'}
page_content='# Microsoft Office
' metadata={'line_number': 1, 'source': '..\\resource\\office_file.mdx'}
page_content='---
' metadata={'line_number': 1, 'source': '..\\resource\\pdf.mdx'}</code></pre>
<h4>通用加载器</h4>
<p><font style="color:rgb(28, 30, 33);">LangChain 拥有一个</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;GenericLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">抽象，它将</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BlobLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">与</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseBlobParser&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">结合在一起。</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;GenericLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">旨在提供标准化的类方法，使现有的</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BlobLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">实现易于使用。目前，仅支持</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;FileSystemBlobLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。</font></p>
<pre><code class="language-python">#示例：doc_blob_loader_generic.py
from langchain_community.document_loaders.generic import GenericLoader
loader = GenericLoader.from_filesystem(
    path=".", glob="*.mdx", show_progress=True, parser=MyParser()
)
for idx, doc in enumerate(loader.lazy_load()):
    if idx &lt; 5:
        print(doc)
print("... output truncated for demo purposes")</code></pre>
<pre><code class="language-plain">100%|██████████| 8/8 [00:00&lt;00:00, 78.69it/s]</code></pre>
<pre><code class="language-plain">page_content='# CSV
' metadata={'line_number': 1, 'source': '..\\resource\\csv.mdx'}
page_content='# File Directory
' metadata={'line_number': 1, 'source': '..\\resource\\file_directory.mdx'}
page_content='# HTML
' metadata={'line_number': 1, 'source': '..\\resource\\html.mdx'}
page_content='---
' metadata={'line_number': 1, 'source': '..\\resource\\index.mdx'}
page_content='# JSON
' metadata={'line_number': 1, 'source': '..\\resource\\json.mdx'}
... output truncated for demo purposes</code></pre>
<h4>自定义通用加载器</h4>
<p><font style="color:rgb(28, 30, 33);">如果您喜欢创建类，您可以子类化并创建一个类来封装逻辑。 您可以从这个类中子类化以使用现有的加载器加载内容。</font></p>
<pre><code class="language-python">#示例：doc_blob_loader_generic_custom.py
from typing import Any
class MyCustomLoader(GenericLoader):
    @staticmethod
    def get_parser(**kwargs: Any) -&gt; BaseBlobParser:
        """Override this method to associate a default parser with the class."""
        return MyParser()</code></pre>
<pre><code class="language-python">loader = MyCustomLoader.from_filesystem(path=".", glob="*.mdx", show_progress=True)
for idx, doc in enumerate(loader.lazy_load()):
    if idx &lt; 5:
        print(doc)
print("... output truncated for demo purposes")</code></pre>
<pre><code class="language-plain">100%|██████████| 8/8 [00:00&lt;00:00, 80.28it/s]</code></pre>
<pre><code class="language-plain">page_content='# CSV
' metadata={'line_number': 1, 'source': '..\\resource\\csv.mdx'}
page_content='# File Directory
' metadata={'line_number': 1, 'source': '..\\resource\\file_directory.mdx'}
page_content='# HTML
' metadata={'line_number': 1, 'source': '..\\resource\\html.mdx'}
page_content='---
' metadata={'line_number': 1, 'source': '..\\resource\\index.mdx'}
page_content='# JSON
' metadata={'line_number': 1, 'source': '..\\resource\\json.mdx'}
... output truncated for demo purposes</code></pre>
<hr />
<h1><font style="color:rgb(28, 30, 33);">自定义对话历史</font>状态管理</h1>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/tech/image-20250424211832514.png" alt="image-20250424211832514" /></p>
<p><font style="color:rgb(28, 30, 33);">之前我们已经介绍了如何添加会话历史记录，但我们仍在手动更新对话历史并将其插入到每个输入中。在真正的问答应用程序中，我们希望有一种持久化对话历史的方式，并且有一种自动插入和更新它的方式。 为此，我们可以使用：</font></p>
<ul>
<li><a href="https://api.python.langchain.com/en/latest/langchain_api_reference.html#module-langchain.memory"><font style="color:rgb(28, 30, 33);">BaseChatMessageHistory</font></a><font style="color:rgb(28, 30, 33);">: 存储对话历史。</font></li>
<li><a href="http://www.aidoczh.com/langchain/v0.2/docs/how_to/message_history/"><font style="color:rgb(28, 30, 33);">RunnableWithMessageHistory</font></a><font style="color:rgb(28, 30, 33);">: LCEL 链和 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BaseChatMessageHistory&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> 的包装器，负责将对话历史注入输入并在每次调用后更新它。 要详细了解如何将这些类结合在一起创建有状态的对话链，请转到 </font><a href="http://www.aidoczh.com/langchain/v0.2/docs/how_to/message_history/"><font style="color:rgb(28, 30, 33);">如何添加消息历史（内存）</font></a><font style="color:rgb(28, 30, 33);"> LCEL 页面。 下面，我们实现了第二种选项的一个简单示例，其中对话历史存储在一个简单的字典中。 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;RunnableWithMessageHistory&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> 的实例会为您管理对话历史。它们接受一个带有键（默认为 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;"session_id"&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">）的配置，该键指定要获取和预置到输入中的对话历史，并将输出附加到相同的对话历史。以下是一个示例：</font></li>
</ul>
<pre><code class="language-python">#示例：custom_chat_session.py
# pip install --upgrade langchain langchain-community langchainhub langchain-chroma bs4
import bs4
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.messages import AIMessage, HumanMessage
from langchain.globals import set_debug
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_history_aware_retriever
from langchain.chains import create_retrieval_chain

# 打印调试日志
set_debug(False)

# 创建一个 WebBaseLoader 对象，用于从指定网址加载文档
loader = WebBaseLoader(
    web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("post-content", "post-title", "post-header")
        )
    ),
)
# 加载文档
docs = loader.load()
# 创建一个 RecursiveCharacterTextSplitter 对象，用于将文档拆分成较小的文本块
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
# 将文档拆分成文本块
splits = text_splitter.split_documents(docs)
# 创建一个 Chroma 对象，用于存储文本块的向量表示
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
# 将向量存储转换为检索器
retriever = vectorstore.as_retriever()

# 定义系统提示词模板
system_prompt = (
    "您是一个用于问答任务的助手。"
    "使用以下检索到的上下文片段来回答问题。"
    "如果您不知道答案，请说您不知道。"
    "最多使用三句话，保持回答简洁。"
    "\n\n"
    "{context}"
)
# 创建一个 ChatPromptTemplate 对象，用于生成提示词
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        ("human", "{input}"),
    ]
)

# 创建一个带有聊天历史记录的提示词模板
qa_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

# 创建一个 ChatOpenAI 对象，表示聊天模型
llm = ChatOpenAI()
# 创建一个问答链
question_answer_chain = create_stuff_documents_chain(llm, prompt)
# 创建一个检索链，将检索器和问答链结合
rag_chain = create_retrieval_chain(retriever, question_answer_chain)

# 定义上下文化问题的系统提示词
contextualize_q_system_prompt = (
    "给定聊天历史和最新的用户问题，"
    "该问题可能引用聊天历史中的上下文，"
    "重新构造一个可以在没有聊天历史的情况下理解的独立问题。"
    "如果需要，不要回答问题，只需重新构造问题并返回。"
)
# 创建一个上下文化问题提示词模板
contextualize_q_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", contextualize_q_system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)
# 创建一个带有历史记录感知的检索器
history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

# 创建一个带有聊天历史记录的问答链
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
# 创建一个带有历史记录感知的检索链
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

# 创建一个字典，用于存储聊天历史记录
store = {}

# 定义一个函数，用于获取指定会话的聊天历史记录
def get_session_history(session_id: str) -&gt; BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 创建一个 RunnableWithMessageHistory 对象，用于管理有状态的聊天历史记录
conversational_rag_chain = RunnableWithMessageHistory(
    rag_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    output_messages_key="answer",
)</code></pre>
<pre><code class="language-python"># 调用有状态的检索链，获取回答
response = conversational_rag_chain.invoke(
    {"input": "什么是任务分解?"},
    config={
        "configurable": {"session_id": "abc123"}
    },  # 在 `store` 中构建一个键为 "abc123" 的键。
)["answer"]
print(response)</code></pre>
<pre><code class="language-python">任务分解是将复杂任务拆分成多个较小、简单的步骤的过程。通过任务分解，代理可以更好地理解任务的各个部分，并事先规划好执行顺序。这可以通过不同的方法实现，如使用提示或指令，或依靠人类输入。</code></pre>
<pre><code class="language-python"># 再次调用有状态的检索链，获取另一个回答
response = conversational_rag_chain.invoke(
    {"input": "我刚刚问了什么?"},
    config={"configurable": {"session_id": "abc123"}},
)["answer"]
print(response)</code></pre>
<pre><code class="language-plain">任务分解是将复杂任务拆分成多个较小、简单的步骤的过程。通过任务分解，代理可以更好地理解任务的各个部分，并事先规划好执行顺序。这可以通过不同的方法实现，如使用提示或指令，或依靠人类输入。</code></pre>
<p>换一个session_id调用，会话不再共享</p>
<pre><code class="language-python"># 再次调用有状态的检索链，换一个session_id
response = conversational_rag_chain.invoke(
    {"input": "我刚刚问了什么?"},
    config={"configurable": {"session_id": "abc456"}},
)["answer"]
print(response)</code></pre>
<pre><code class="language-plain">您最近询问了有关一个经典平台游戏的信息，其中主角是名叫Mario的管道工，游戏共有10个关卡，主角可以行走和跳跃，需要避开障碍物和敌人的攻击。</code></pre>
<p><font style="color:rgb(28, 30, 33);">对话历史可以在 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;store&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> 字典中检查：</font></p>
<pre><code class="language-python"># 打印存储在会话 "abc123" 中的所有消息
for message in store["abc123"].messages:
    if isinstance(message, AIMessage):
        prefix = "AI"
    else:
        prefix = "User"
    print(f"{prefix}: {message.content}\n")</code></pre>
<pre><code class="language-plain">User: 什么是任务分解?

AI: 任务分解是将复杂任务拆分成多个较小、简单的步骤的过程。通过任务分解，代理可以更好地理解任务的各个部分，并事先规划好执行顺序。这可以通过不同的方法实现，如使用提示或指令，或依靠人类输入。

User: 我刚刚问了什么?

AI: 您刚刚问了关于任务分解的问题。任务分解是将复杂任务拆分成多个较小、简单的步骤的过程。这有助于代理更好地理解任务并规划执行顺序。</code></pre>
<p><font style="color:rgb(28, 30, 33);"> </font></p>]]></description>
    <pubDate>Thu, 24 Apr 2025 21:19:11 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=23</guid>
</item>
<item>
    <title>ModeScope在线训练平台&amp;服务器选配训练模型</title>
    <link>https://blog.sayyou.icu/?post=22</link>
    <description><![CDATA[<p>模型微调的基本模式</p>
<ul>
<li>全量微调</li>
<li>局部微调</li>
<li>增量微调<br />
Model Scope 在线训练平台介绍</li>
</ul>
<p>使用 Model Scope 在线训练 GPT2</p>
<p>选配AutoDL服务器并使用vscode远程连接</p>
<p>AutoDL网站地址: <a href="https://www.autodl.com/home">https://www.autodl.com/home</a><br />
通过VSCode和SSH使用AutoDL服务器训练模型教程地址: <a href="https://www.autodl.com/home">https://www.autodl.com/home</a><br />
混合精度训练(Mixed Precision Training)</p>
<p>介绍<br />
优点<br />
FP16和 FP32<br />
FP16(16位浮点数)<br />
FP32(32位浮点数)<br />
基本流程<br />
准备环境<br />
GradScaler</p>
<ol>
<li>模型微调的基本模式<br />
全量微调<br />
对所有参数进行微调。<br />
要求较高的计算资源(算力和显存)。<br />
效果最佳,适合复杂场景。</li>
</ol>
<p>局部微调<br />
仅微调模型的某些部分,如输出层或特定的Transformer 层。<br />
计算资源需求较小,适合有限资源的情况。</p>
<p>增量微调<br />
新增参数进行微调,将新知识存储在新增参数中。<br />
计算资源需求最低,但效果不如全量微调。</p>
<ol start="2">
<li>Model Scope 在线训练平台介绍<br />
Model Scope 是一个在线的AI模型训练平台,支持GPT2、BERT等多种模型的训练与部署。其优势包括:</li>
</ol>
<p>提供云端训练环境,降低本地计算资源压力。<br />
支持丰富的预训练模型和数据集。<br />
简化微调训练流程,适合快速迭代开发。</p>
<ol start="3">
<li>使用 Model Scope 在线训练 GPT2<br />
可以使用 Model Scope 在线平台进行GPT2的模型微调,以下是具体步骤:</li>
</ol>
<p>登录 Model Scope 平台,选择GPT2 模型。<br />
上传或选择中文古诗词训练数据集。<br />
配置模型微调参数(如学习率、训练轮数等)。<br />
启动训练,并在训练完成后下载微调后的模型权重。</p>
<ol start="4">
<li>选配AutoDL服务器并使用vscode远程连接<br />
AutoDL网站地址: <a href="https://www.autodl.com/home">https://www.autodl.com/home</a><br />
通过VSCode和SSH使用AutoDL服务器训练模型教程地址: <a href="https://www.autodl.com/home">https://www.autodl.com/home</a></li>
<li>混合精度训练(Mixed Precision Training)<br />
介绍<br />
混合精度训练(Mixed Precision Training)是一种在深度学习中提高训练速度和减少内存占用的技术。在PyTorch中,通过使用半精度浮点数(16位浮点数,FP16)和单精度浮点数(32位浮点数,FP32)的组合。</li>
</ol>
<p>优点<br />
在不改变模型、不降低模型训练精度的前提下,可以缩短训练时间,降低存储需求,因而能支持更大的batch size、更大模型和尺寸更大的输入的训练。</p>
<p>FP16 和 FP32<br />
FP16和FP32 是两种不同的浮点数表示格式,它们表示浮点数的精度和范围。</p>
<p>FP16 (16位浮点数):<br />
FP16 是一种半精度浮点数格式,它使用16位(2字节)来表示一个浮点数。<br />
它的格式通常包括1位符号位、5位指数位和10位尾数位。<br />
由于指数位较少,FP16能够表示的数值范围比FP32小,但它需要的内存和计算资源也更少。<br />
FP16在深度学习中被用于加速计算和节省内存,尤其是在支持FP16运算的硬件上。</p>
<p>FP32(32位浮点数):<br />
FP32 是一种单精度浮点数格式,它使用32位(4字节)来表示一个浮点数。<br />
它的格式包括1位符号位、8位指数位和23位尾数位。<br />
相比于FP16,FP32能够表示更大范围的数值,具有更高的精度,但也需要更多的内存和计算资源。<br />
FP32是最常用的浮点数类型,适用于广泛的科学计算和工程应用。</p>
<p>在深度学习中,使用FP16进行训练可以显著减少模型的内存占用,加快数据传输和计算速度,尤其是在配备有Tensor Core的NVIDIA GPU上。然而,由于FP16的数值范围较小,可能会导致数值下溢(underflow)或精度损失,因此在训练过程中可能需要一些特殊的技术(如梯度缩放和混合精度训练)来确保模型的数值稳定性和最终精度。</p>
<p>基本流程<br />
下面是一个使用PyTorch进行混合精度训练的例子:</p>
<ol>
<li>准备环境:</li>
</ol>
<p>首先,确保你的硬件和PyTorch版本支持FP16运算。然后,导入必要的库:</p>
<p>Python</p>
<p>import torch<br />
import torch.nn as nn<br />
import torch.optim as optim<br />
from torch.cuda.amp import autocast, GradScaler</p>
<ol start="2">
<li>定义模型:</li>
</ol>
<p>创建一个简单的神经网络模型,例如一个多层感知机(MLP):</p>
<p>Python</p>
<p>class SimpleMLP(nn.Module):<br />
def <strong>init</strong>(self):<br />
super(SimpleMLP, self).<strong>init</strong>()<br />
self.fc1 = nn.Linear(10, 5)<br />
self.fc2 = nn.Linear(5, 2)</p>
<pre><code>def forward(self, x):
    x = torch.relu(self.fc1(x))
    x = self.fc2(x)
    return x</code></pre>
<ol start="3">
<li>启用混合精度:</li>
</ol>
<p>使用autocast()上下文管理器来指定哪些操作应该使用FP16执行:</p>
<p>Python</p>
<p>model = SimpleMLP().cuda()<br />
model.train()<br />
scaler = GradScaler()</p>
<p>for epoch in range(num_epochs):<br />
for batch in data_loader:<br />
x, y = batch<br />
x, y = x.cuda(), y.cuda()</p>
<pre><code>    with autocast():
        outputs = model(x)
        loss = criterion(outputs, y)

    # 反向传播和权重更新
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()</code></pre>
<p>在这个例子中,autocast()将模型的前向传播和损失计算转换为FP16格式。然而,反向传播仍然是在FP32精度下进行的,这是为了保持数值稳定性。</p>
<ol start="4">
<li>使用GradScaler:</li>
</ol>
<p>由于FP16的数值范围较小,可能会导致梯度下溢(underflow)。GradScaler在反向传播之前将梯度的值放大,然后在权重更新之后将其缩放回来:</p>
<p>Python</p>
<p>scaler = GradScaler()<br />
在计算梯度后,使用scaler.step(optimizer)来应用缩放后的梯度。</p>
<p>GradScaler<br />
GradScaler 是 PyTorch 中 torch.cuda.amp 模块提供的一个工具,它用于帮助进行混合精度训练。在混合精度训练中,我们通常使用FP16来存储模型的权重和进行前向计算,以减少内存占用和加速计算。</p>
<p>然而,FP16的数值范围比FP32小,这可能导致在梯度计算和权重更新时出现数值下溢(underflow),即梯度的数值变得非常小,以至于在FP16格式下无法有效表示。</p>
<p>GradScaler 通过在反向传播之前自动放大(scale up)梯度的值来解决这个问题。然后,在执行权重更新之后,GradScaler 会将放大的梯度缩放(scale down)回原来的大小。这个过程确保了即使在FP16格式下,梯度的数值也能保持在可表示的范围内,从而避免了数值下溢的问题。</p>
<p>Python</p>
<p>scaler = torch.cuda.amp.GradScaler()</p>
<p>for inputs, targets in dataloader:<br />
with autocast():<br />
outputs = model(inputs)<br />
loss = loss_fn(outputs, targets)</p>
<pre><code>scaler.scale(loss).backward()  # 放大梯度
scaler.step(optimizer)  # 应用缩放后的梯度进行权重更新
scaler.update()  # 更新缩放因子</code></pre>
<p>保存和加载模型:<br />
Python</p>
<p>torch.save(model.state_dict(), 'model.pth')<br />
model.load_state_dict(torch.load('model.pth'))<br />
在混合精度训练中,虽然模型的权重在训练过程中可能会被转换为FP16格式以节省内存和加速计算,但在保存模型时,我们通常会将权重转换回 FP32格式。这是因为FP32提供了更高的数值精度和更广泛的硬件支持,这使得模型在不同环境中的兼容性和可靠性更好。</p>
<p>在PyTorch 中,当你调用 model.state_dict()方法时,默认情况下它会返回一个包含权重的字典。即使你在训练时使用了FP16,这个字典也会包含FP32权重,因为PyTorch 会先转换为FP32 再保存。同样,当你使用 torch.load()加载模型时,如果模型权重是FP16格式,PyTorch 会自动将它们转换为FP32。</p>
<p>注意,如果你的模型是在GPU上训练的,加载模型时应该使用map_location 参数来指定加载到CPU,然后再将模型转换为FP32并移回 GPU。</p>]]></description>
    <pubDate>Wed, 23 Apr 2025 22:29:07 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=22</guid>
</item>
<item>
    <title>RAG 简介</title>
    <link>https://blog.sayyou.icu/?post=19</link>
    <description><![CDATA[<h2><font style="color:#000000;">什么是检索增强生成？</font></h2>
<p><font style="color:#000000;">检索增强生成（RAG）是指对大型语言模型输出进行优化，使其能够在生成响应之前引用训练数据来源之外的权威知识库。大型语言模型（LLM）用海量数据进行训练，使用数十亿个参数为回答问题、翻译语言和完成句子等任务生成原始输出。在 LLM 本就强大的功能基础上，RAG 将其扩展为能访问特定领域或组织的内部知识库，所有这些都无需重新训练模型。这是一种经济高效地改进 LLM 输出的方法，让它在各种情境下都能保持相关性、准确性和实用性。</font></p>
<h2><font style="color:#000000;background-color:rgb(251, 251, 251);">为什么检索增强生成很重要？</font></h2>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">LLM 是一项关键的人工智能（AI）技术，为智能聊天机器人和其他自然语言处理（NLP）应用程序提供支持。目标是通过交叉引用权威知识来源，创建能够在各种环境中回答用户问题的机器人。不幸的是，LLM 技术的本质在 LLM 响应中引入了不可预测性。此外，LLM 训练数据是静态的，并引入了其所掌握知识的截止日期。</font></p>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">LLM 面临的已知挑战包括：</font></p>
<ul>
<li><font style="color:#000000;background-color:rgb(251, 251, 251);">在没有答案的情况下提供虚假信息。</font></li>
<li><font style="color:#000000;background-color:rgb(251, 251, 251);">当用户需要特定的当前响应时，提供过时或通用的信息。</font></li>
<li><font style="color:#000000;background-color:rgb(251, 251, 251);">从非权威来源创建响应。</font></li>
<li><font style="color:#000000;background-color:rgb(251, 251, 251);">由于术语混淆，不同的培训来源使用相同的术语来谈论不同的事情，因此会产生不准确的响应。</font></li>
</ul>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">您可以将</font><a href="https://aws.amazon.com/what-is/large-language-model/"><font style="color:#000000;background-color:rgb(251, 251, 251);">大型语言模型</font></a><font style="color:#000000;background-color:rgb(251, 251, 251);">看作是一个过于热情的新员工，他拒绝随时了解时事，但总是会绝对自信地回答每一个问题。不幸的是，这种态度会对用户的信任产生负面影响，这是您不希望聊天机器人效仿的！</font></p>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">RAG 是解决其中一些挑战的一种方法。它会重定向 LLM，从权威的、预先确定的知识来源中检索相关信息。组织可以更好地控制生成的文本输出，并且用户可以深入了解 LLM 如何生成响应。</font></p>
<h2><font style="color:#000000;">检索增强生成有哪些好处？</font></h2>
<p><font style="color:#000000;">RAG 技术为组织的</font><a href="https://aws.amazon.com/what-is/generative-ai/"><font style="color:#000000;">生成式人工智能</font></a><font style="color:#000000;">工作带来了多项好处。</font></p>
<h3><strong><font style="color:#000000;">经济高效的实施</font></strong></h3>
<p><font style="color:#000000;">聊天机器人开发通常从</font><a href="https://aws.amazon.com/what-is/foundation-models/"><font style="color:#000000;">基础模型</font></a><font style="color:#000000;">开始。基础模型（FM）是在广泛的广义和未标记数据上训练的 API 可访问 LLM。针对组织或领域特定信息重新训练 FM 的计算和财务成本很高。RAG 是一种将新数据引入 LLM 的更加经济高效的方法。它使生成式人工智能技术更广泛地获得和使用。</font></p>
<h3><strong><font style="color:#000000;">当前信息</font></strong></h3>
<p><font style="color:#000000;">即使 LLM 的原始训练数据来源适合您的需求，但保持相关性也具有挑战性。RAG 允许开发人员为生成模型提供最新的研究、统计数据或新闻。他们可以使用 RAG 将 LLM 直接连接到实时社交媒体提要、新闻网站或其他经常更新的信息来源。然后，LLM 可以向用户提供最新信息。</font></p>
<h3><strong><font style="color:#000000;">增强用户信任度</font></strong></h3>
<p><font style="color:#000000;">RAG 允许 LLM 通过来源归属来呈现准确的信息。输出可以包括对来源的引文或引用。如果需要进一步说明或更详细的信息，用户也可以自己查找源文档。这可以增加对您的生成式人工智能解决方案的信任和信心。</font></p>
<h3><strong><font style="color:#000000;">更多开发人员控制权</font></strong></h3>
<p><font style="color:#000000;">借助 RAG，开发人员可以更高效地测试和改进他们的聊天应用程序。他们可以控制和更改 LLM 的信息来源，以适应不断变化的需求或跨职能使用。开发人员还可以将敏感信息的检索限制在不同的授权级别内，并确保 LLM 生成适当的响应。此外，如果 LLM 针对特定问题引用了错误的信息来源，他们还可以进行故障排除并进行修复。组织可以更自信地为更广泛的应用程序实施生成式人工智能技术。</font></p>
<h2><font style="color:#000000;background-color:rgb(251, 251, 251);">检索增强生成的工作原理是什么？</font></h2>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">如果没有 RAG，LLM 会接受用户输入，并根据它所接受训练的信息或它已经知道的信息创建响应。RAG 引入了一个信息检索组件，该组件利用用户输入首先从新数据源提取信息。用户查询和相关信息都提供给 LLM。LLM 使用新知识及其训练数据来创建更好的响应。以下各部分概述了该过程。</font></p>
<h3><strong><font style="color:#000000;background-color:rgb(251, 251, 251);">创建外部数据</font></strong></h3>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">LLM 原始训练数据集之外的新数据称为</font><em><font style="color:#000000;background-color:rgb(251, 251, 251);">外部数据</font></em><font style="color:#000000;background-color:rgb(251, 251, 251);">。它可以来自多个数据来源，例如 API、数据库或文档存储库。数据可能以各种格式存在，例如文件、数据库记录或长篇文本。另一种称为</font><em><font style="color:#000000;background-color:rgb(251, 251, 251);">嵌入语言模型</font></em><font style="color:#000000;background-color:rgb(251, 251, 251);">的 AI 技术将数据转换为数字表示形式并将其存储在向量数据库中。这个过程会创建一个生成式人工智能模型可以理解的知识库。</font></p>
<h3><strong><font style="color:#000000;background-color:rgb(251, 251, 251);">检索相关信息</font></strong></h3>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">下一步是执行相关性搜索。用户查询将转换为向量表示形式，并与向量数据库匹配。例如，考虑一个可以回答组织的人力资源问题的智能聊天机器人。如果员工搜索</font><em><font style="color:#000000;background-color:rgb(251, 251, 251);">：“我有多少年假？”</font></em><font style="color:#000000;background-color:rgb(251, 251, 251);">，系统将检索年假政策文件以及员工个人过去的休假记录。这些特定文件将被退回，因为它们与员工输入的内容高度相关。相关性是使用数学向量计算和表示法计算和建立的。</font></p>
<h3><strong><font style="color:#000000;background-color:rgb(251, 251, 251);">增强 LLM 提示</font></strong></h3>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">接下来，RAG 模型通过在上下文中添加检索到的相关数据来增强用户输入（或提示）。此步骤使用提示工程技术与 LLM 进行有效沟通。增强提示允许大型语言模型为用户查询生成准确的答案。</font></p>
<h3><strong><font style="color:#000000;background-color:rgb(251, 251, 251);">更新外部数据</font></strong></h3>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">下一个问题可能是：如果外部数据过时了怎么办？ 要维护当前信息以供检索，请异步更新文档并更新文档的嵌入表示形式。您可以通过自动化实时流程或定期批处理来执行此操作。这是数据分析中常见的挑战：可以使用不同的数据科学方法进行变更管理。</font></p>
<p><font style="color:#000000;background-color:rgb(251, 251, 251);">下图显示了将 RAG 与 LLM 配合使用的概念流程。</font></p>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/tech/image-20250419151623211.png" alt="image-20250419151623211" /></p>
<h2><font style="color:#000000;">检索增强生成和语义搜索有什么区别？</font></h2>
<p><font style="color:#000000;">语义搜索可以提高 RAG 结果，适用于想要在其 LLM 应用程序中添加大量外部知识源的组织。现代企业在各种系统中存储大量信息，例如手册、常见问题、研究报告、客户服务指南和人力资源文档存储库等。上下文检索在规模上具有挑战性，因此会降低生成输出质量。</font></p>
<p><font style="color:#000000;">语义搜索技术可以扫描包含不同信息的大型数据库，并更准确地检索数据。例如，他们可以回答诸如</font><font style="color:#000000;"> </font><em><font style="color:#000000;">“去年在机械维修上花了多少钱？”</font></em><font style="color:#000000;">之类的问题，方法是将问题映射到相关文档并返回特定文本而不是搜索结果。然后，开发人员可以使用该答案为 LLM 提供更多上下文。</font></p>
<p><font style="color:#000000;">RAG 中的传统或关键字搜索解决方案对知识密集型任务产生的结果有限。开发人员在手动准备数据时还必须处理单词嵌入、文档分块和其他复杂问题。相比之下，语义搜索技术可以完成知识库准备的所有工作，因此开发人员不必这样做。它们还生成语义相关的段落和按相关性排序的标记词，以最大限度地提高 RAG 有效载荷的质量。</font></p>
<h1><font style="color:#000000;">Document loaders和Text splitters</font></h1>
<h2><font style="color:#000000;">Document loaders(文档加载器)</font></h2>
<p><font style="color:#000000;">Document loaders(文档加载器) </font><font style="color:rgb(28, 30, 33);">这些类加载文档对象。LangChain与各种数据源有数百个集成，可以从中加载数据：Slack、Notion、Google Drive等。 每个文档加载器都有自己特定的参数，但它们可以通过相同的方式使用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;.load&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">方法调用。 以下是一个示例用法：</font></p>
<pre><code class="language-python">from langchain_community.document_loaders.csv_loader import CSVLoader
loader = CSVLoader(
    ...  # &lt;-- 在这里添加特定于集成的参数
)
data = loader.load()</code></pre>
<h3>如何加载 CSV 文件</h3>
<p><a href="https://zh.wikipedia.org/wiki/%E9%80%97%E5%8F%B7%E5%88%86%E9%9A%94%E5%80%BC"><font style="color:rgb(28, 30, 33);">逗号分隔值（CSV）</font></a><font style="color:rgb(28, 30, 33);">文件是一种使用逗号分隔值的定界文本文件。文件的每一行是一个数据记录。每个记录由一个或多个字段组成，字段之间用逗号分隔。</font></p>
<p><font style="color:rgb(28, 30, 33);">LangChain 实现了一个</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.csv_loader.CSVLoader.html"><font style="color:rgb(28, 30, 33);">CSV 加载器</font></a><font style="color:rgb(28, 30, 33);">，可以将 CSV 文件加载为一系列</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://api.python.langchain.com/en/latest/documents/langchain_core.documents.base.Document.html#langchain_core.documents.base.Document"><font style="color:rgb(28, 30, 33);">Document</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">对象。CSV 文件的每一行都会被翻译为一个文档。</font></p>
<pre><code class="language-python">#示例：csv_loader.py
from langchain_community.document_loaders.csv_loader import CSVLoader

file_path = (
    "../../resource/doc_search.csv"
)
loader = CSVLoader(file_path=file_path,encoding="UTF-8")
data = loader.load()
for record in data[:2]:
    print(record)</code></pre>
<pre><code class="language-python">page_content='名称: 狮子
种类: 哺乳动物
年龄: 8
栖息地: 非洲草原' metadata={'source': '../../resource/doc_search.csv', 'row': 0}
page_content='名称: 大熊猫
种类: 哺乳动物
年龄: 5
栖息地: 中国竹林' metadata={'source': '../../resource/doc_search.csv', 'row': 1}</code></pre>
<h4>自定义 CSV 解析和加载</h4>
<p><code>&lt;font style="color:rgb(28, 30, 33);"&gt;CSVLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">接受一个</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;csv_args&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">关键字参数，用于自定义传递给 Python 的</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;csv.DictReader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">的参数。有关支持的 csv 参数的更多信息，请参阅</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://docs.python.org/zh-cn/3/library/csv.html"><font style="color:rgb(28, 30, 33);">csv 模块</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">文档。</font></p>
<pre><code class="language-python">#示例：csv_custom.py
from langchain_community.document_loaders.csv_loader import CSVLoader

file_path = (
    "../../resource/doc_search.csv"
)
loader = CSVLoader(
    file_path=file_path,
    encoding="UTF-8",
    csv_args={
        "delimiter": ",",
        "quotechar": '"',
        "fieldnames": ["Name", "Species", "Age", "Habitat"],
    },
)
data = loader.load()
for record in data[:2]:
    print(record)</code></pre>
<pre><code class="language-plain">page_content='Name: 名称
Species: 种类
Age: 年龄
Habitat: 栖息地' metadata={'source': '../../resource/doc_search.csv', 'row': 0}
page_content='Name: 狮子
Species: 哺乳动物
Age: 8
Habitat: 非洲草原' metadata={'source': '../../resource/doc_search.csv', 'row': 1}</code></pre>
<h3>如何加载 HTML</h3>
<p><font style="color:rgb(28, 30, 33);">超文本标记语言（HTML）是用于在Web浏览器中显示的文档的标准标记语言。</font></p>
<p><font style="color:rgb(28, 30, 33);">这里介绍了如何将HTML文档加载到LangChain的</font><a href="https://api.python.langchain.com/en/latest/documents/langchain_core.documents.base.Document.html#langchain_core.documents.base.Document"><font style="color:rgb(28, 30, 33);">Document</font></a><font style="color:rgb(28, 30, 33);">对象中，以便我们可以在下游使用。</font></p>
<p><font style="color:rgb(28, 30, 33);">解析HTML文件通常需要专门的工具。在这里，我们演示了如何通过</font><a href="https://unstructured-io.github.io/unstructured/"><font style="color:rgb(28, 30, 33);">Unstructured</font></a><font style="color:rgb(28, 30, 33);">和</font><a href="https://beautiful-soup-4.readthedocs.io/en/latest/"><font style="color:rgb(28, 30, 33);">BeautifulSoup4</font></a><font style="color:rgb(28, 30, 33);">进行解析，可以通过pip安装。请前往集成页面查找与其他服务的集成，例如</font><a href="http://www.aidoczh.com/langchain/v0.2/docs/integrations/document_loaders/azure_document_intelligence/"><font style="color:rgb(28, 30, 33);">Azure AI Document Intelligence</font></a><font style="color:rgb(28, 30, 33);">或</font><a href="http://www.aidoczh.com/langchain/v0.2/docs/integrations/document_loaders/firecrawl/"><font style="color:rgb(28, 30, 33);">FireCrawl</font></a><font style="color:rgb(28, 30, 33);">。</font></p>
<h4>使用Unstructured加载HTML</h4>
<pre><code class="language-python">%pip install "unstructured[html]"</code></pre>
<pre><code class="language-python">#示例：html_loader.py
from langchain_community.document_loaders import UnstructuredHTMLLoader

file_path = "../../resource/content.html"
loader = UnstructuredHTMLLoader(file_path, encodings="UTF-8")
data = loader.load()
print(data)</code></pre>
<pre><code class="language-plain">[Document(metadata={'source': '../../resource/content.html'}, page_content='风景展示\n\n黄山\n\n黄山位于中国安徽省南部，是中国著名的风景名胜区，以奇松、怪石、云海和温泉“四绝”闻名。\n\n大峡谷\n\n大峡谷位于美国亚利桑那州，是世界上最著名的自然景观之一，以其壮观的地质奇观和深邃的峡谷闻名。')]</code></pre>
<h4>使用BeautifulSoup4加载HTML</h4>
<p><font style="color:rgb(28, 30, 33);">我们还可以使用BeautifulSoup4使用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;BSHTMLLoader&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">加载HTML文档。这将将HTML中的文本提取到</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;page_content&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">中，并将页面标题提取到</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;metadata&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">的</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;title&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">中。</font></p>
<pre><code class="language-python">#pip install bs4</code></pre>
<pre><code class="language-python">#示例：html_bs4.py
from langchain_community.document_loaders import BSHTMLLoader

file_path = "../../resource/content.html"
loader = BSHTMLLoader(file_path, open_encoding="UTF-8")
data = loader.load()
print(data)</code></pre>
<pre><code class="language-plain">[Document(metadata={'source': '../../resource/content.html', 'title': '风景展示'}, page_content='\n\n\n\n风景展示\n\n\n\n风景展示\n\n黄山\n黄山位于中国安徽省南部，是中国著名的风景名胜区，以奇松、怪石、云海和温泉“四绝”闻名。\n\n\n\n大峡谷\n大峡谷位于美国亚利桑那州，是世界上最著名的自然景观之一，以其壮观的地质奇观和深邃的峡谷闻名。\n\n\n\n')]</code></pre>
<hr />
<h3>如何加载 PDF文件</h3>
<p><a href="https://zh.wikipedia.org/wiki/PDF"><font style="color:rgb(28, 30, 33);">便携式文档格式（PDF）</font></a><font style="color:rgb(28, 30, 33);">是由Adobe于1992年开发的一种文件格式，标准化为ISO 32000。它以一种与应用软件、硬件和操作系统无关的方式呈现文档，包括文本格式和图像。</font></p>
<p><font style="color:rgb(28, 30, 33);">本指南介绍了如何将PDF文档加载到我们在下游使用的LangChain</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://api.python.langchain.com/en/latest/documents/langchain_core.documents.base.Document.html#langchain_core.documents.base.Document"><font style="color:rgb(28, 30, 33);">Document</font></a><font style="color:rgb(28, 30, 33);">格式中。</font></p>
<p><font style="color:rgb(28, 30, 33);">LangChain集成了许多PDF解析器。有些解析器简单且相对低级，而其他解析器支持OCR和图像处理，或进行高级文档布局分析。选择合适的解析器将取决于您的应用程序。下面我们列举了一些可能的选择。</font></p>
<h4>使用PyPDF</h4>
<p><font style="color:rgb(28, 30, 33);">这里我们使用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;pypdf&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">将PDF加载为文档数组，其中每个文档包含页面内容和带有</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;page&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">编号的元数据。</font></p>
<pre><code class="language-python">%pip install pypdf</code></pre>
<pre><code class="language-python">#示例：pdf_loader.py
from langchain_community.document_loaders import PyPDFLoader
file_path = ("../../resource/pytorch.pdf")
loader = PyPDFLoader(file_path)
pages = loader.load_and_split()
print(pages[0])</code></pre>
<pre><code class="language-plain">page_content='PyTorch: An Imperative Style, High-Performance
Deep Learning Library
Adam Paszke
University of Warsaw
adam.paszke@gmail.comSam Gross
Facebook AI Research
sgross@fb.comFrancisco Massa
Facebook AI Research
fmassa@fb.com
Adam Lerer
Facebook AI Research
alerer@fb.comJames Bradbury
Google
jekbradbury@gmail.comGregory Chanan
Facebook AI Research
gchanan@fb.com
Trevor Killeen
Self Employed
killeent@cs.washington.eduZeming Lin
Facebook AI Research
zlin@fb.comNatalia Gimelshein
NVIDIA
ngimelshein@nvidia.com
Luca Antiga
Orobix
luca.antiga@orobix.comAlban Desmaison
Oxford University
alban@robots.ox.ac.ukAndreas Köpf
Xamla
andreas.koepf@xamla.com
Edward Yang
Facebook AI Research
ezyang@fb.comZach DeVito
Facebook AI Research
zdevito@cs.stanford.eduMartin Raison
Nabla
martinraison@gmail.com
Alykhan Tejani
Twitter
atejani@twitter.comSasank Chilamkurthy
Qure.ai
sasankchilamkurthy@gmail.comBenoit Steiner
Facebook AI Research
benoitsteiner@fb.com
Lu Fang
Facebook
lufang@fb.comJunjie Bai
Facebook
jbai@fb.comSoumith Chintala
Facebook AI Research
soumith@gmail.com
Abstract
Deep learning frameworks have often focused on either usability or speed, but
not both. PyTorch is a machine learning library that shows that these two goals
are in fact compatible: it provides an imperative and Pythonic programming style
that supports code as a model, makes debugging easy and is consistent with other
popular scientiﬁc computing libraries, while remaining efﬁcient and supporting
hardware accelerators such as GPUs.
In this paper, we detail the principles that drove the implementation of PyTorch
and how they are reﬂected in its architecture. We emphasize that every aspect of
PyTorch is a regular Python program under the full control of its user. We also
explain how the careful and pragmatic implementation of the key components of
its runtime enables them to work together to achieve compelling performance.
We demonstrate the efﬁciency of individual subsystems, as well as the overall
speed of PyTorch on several common benchmarks.
33rd Conference on Neural Information Processing Systems (NeurIPS 2019), Vancouver, Canada.' metadata={'source': '../../resource/pytorch.pdf', 'page': 0}</code></pre>
<p><font style="color:rgb(28, 30, 33);">这种方法的优点是可以通过页码检索文档。</font></p>
<h5>对PDF进行向量搜索</h5>
<p><font style="color:rgb(28, 30, 33);">一旦我们将PDF加载到LangChain的</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;Document&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">对象中，我们可以像通常一样对它们进行索引（例如，RAG应用程序）。</font></p>
<pre><code class="language-python">#示例：pdf_search.py
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
faiss_index = FAISS.from_documents(pages, OpenAIEmbeddings())
docs = faiss_index.similarity_search("What is LayoutParser?", k=2)
for doc in docs:
    print(str(doc.metadata["page"]) + ":", doc.page_content[:300])</code></pre>
<pre><code class="language-plain">0: PyTorch: An Imperative Style, High-Performance
Deep Learning Library
Adam Paszke
University of Warsaw
adam.paszke@gmail.comSam Gross
Facebook AI Research
sgross@fb.comFrancisco Massa
Facebook AI Research
fmassa@fb.com
Adam Lerer
Facebook AI Research
alerer@fb.comJames Bradbury
Google
jekbradbury@gma
1: 1 Introduction
With the increased interest in deep learning in recent years, there has been an explosion of machine
learning tools. Many popular frameworks such as Caffe [ 1], CNTK [ 2], TensorFlow [ 3], and
Theano [ 4], construct a static dataﬂow graph that represents the computation and which can 
</code></pre>
<p>从图像中提取文本一些 PDF 包含文本图像，例如扫描文档或图表。使用 <code>rapidocr-onnxruntime</code> 软件包，我们也可以将图像提取为文本：</p>
<pre><code class="language-python">#示例：pdf_image_text.py
#pip install rapidocr-onnxruntime
file_path = ("../../resource/pytorch.pdf")
loader = PyPDFLoader(file_path, extract_images=True)
pages = loader.load()
#识别第9页图片文字
print(pages[8].page_content)</code></pre>
<pre><code class="language-plain">6.4 Adoption
The validity of design decisions and their impact on ease-of-use is hard to measure. As a proxy,
we tried to quantify how well the machine learning community received PyTorch by counting how
often various machine learning tools (including Caffe, Chainer, CNTK, Keras, MXNet, PyTorch,
TensorFlow, and Theano) are mentioned on arXiv e-Prints since the initial release of PyTorch in
January 2017. In Figure 3 we report the monthly number of mentions of the word "PyTorch" as a
percentage of all mentions among these deep learning frameworks. We counted tools mentioned
multiple times in a given paper only once, and made the search case insensitive to account for various
spellings.
Figure 3: Among arXiv papers each month that mention common deep learning frameworks, percentage of
them that mention PyTorch.
7 Conclusion and future work
PyTorch has become a popular tool in the deep learning research community by combining a focus
on usability with careful performance considerations. In addition to continuing to support the latest
trends and advances in deep learning, in the future we plan to continue to improve the speed and
scalability of PyTorch. Most notably, we are working on the PyTorch JIT: a suite of tools that
allow PyTorch programs to be executed outside of the Python interpreter where they can be further
optimized. We also intend to improve support for distributed computation by providing efﬁcient
primitives for data parallelism as well as a Pythonic library for model parallelism based around
remote procedure calls.
8 Acknowledgements
We are grateful to the PyTorch community for their feedback and contributions that greatly inﬂuenced
the design and implementation of PyTorch. We thank all the PyTorch core team members, contributors
and package maintainers including Ailing Zhang, Alex Suhan, Alfredo Mendoza, Alican Bozkurt,
Andrew Tulloch, Ansha Yu, Anthony Shoumikhin, Bram Wasti, Brian Vaughan, Christian Puhrsch,
David Reiss, David Riazati, Davide Libenzi, Dmytro Dzhulgakov, Dwaraj Rajagopal, Edward Yang,
Elias Ellison, Fritz Obermeyer, George Zhang, Hao Lu, Hong Xu, Hung Duong, Igor Fedan, Ilia
Cherniavskii, Iurii Zdebskyi, Ivan Kobzarev, James Reed, Jeff Smith, Jerry Chen, Jerry Zhang, Jiakai
Liu, Johannes M. Dieterich, Karl Ostmo, Lin Qiao, Martin Yuan, Michael Suo, Mike Ruberry, Mikhail
Zolothukhin, Mingzhe Li, Neeraj Pradhan, Nick Korovaiko, Owen Anderson, Pavel Belevich, Peter
Johnson, Pritam Damania, Raghuraman Krishnamoorthi, Richard Zou, Roy Li, Rui Zhu, Sebastian
Messmer, Shen Li, Simon Wang, Supriya Rao, Tao Xu, Thomas Viehmann, Vincent Quenneville-
Belair, Vishwak Srinivasan, Vitaly Fedyunin, Wanchao Liang, Wei Yang, Will Feng, Xiaomeng Yang,
Xiaoqiang Zheng, Xintao Chen, Yangqing Jia, Yanli Zhao, Yinghai Lu and Zafar Takhirov.
References
[1]Yangqing Jia, Evan Shelhamer, Jeff Donahue, Sergey Karayev, Jonathan Long, Ross Girshick,
Sergio Guadarrama, and Trevor Darrell. Caffe: Convolutional architecture for fast feature
embedding. arXiv preprint arXiv:1408.5093 , 2014.
[2]Frank Seide and Amit Agarwal. Cntk: Microsoft’s open-source deep-learning toolkit. In
Proceedings of the 22Nd ACM SIGKDD International Conference on Knowledge Discovery
and Data Mining , KDD ’16, pages 2135–2135, New York, NY , USA, 2016. ACM.
950%
40%
30%
20%
10%
0%
Jul2017
Jan2018
Jul2018
Jan2019</code></pre>
<h2>Text splitters(文本分割器)</h2>
<h3>如何递归分割文本</h3>
<p><font style="color:rgb(28, 30, 33);">递归分割(recursively)，这个文本分割器是用于通用文本的推荐工具。它接受一个字符列表作为参数。它会按顺序尝试在这些字符上进行分割，直到块足够小。默认的字符列表是 </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;["\n\n", "\n", " ", ""]&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。这样做的效果是尽可能保持所有段落（然后是句子，再然后是单词）在一起，因为这些通常看起来是语义上相关的文本块。</font></p>
<ol>
<li><font style="color:rgb(28, 30, 33);">文本如何分割：根据字符列表。</font></li>
<li><font style="color:rgb(28, 30, 33);">块大小如何衡量：根据字符数量。</font></li>
</ol>
<p><font style="color:rgb(28, 30, 33);">下面我们展示一个使用示例。</font></p>
<p><font style="color:rgb(28, 30, 33);">要直接获取字符串内容，请使用</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;.split_text&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。</font></p>
<p><font style="color:rgb(28, 30, 33);">要创建 LangChain</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://api.python.langchain.com/en/latest/documents/langchain_core.documents.base.Document.html"><font style="color:rgb(28, 30, 33);">Document</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">对象（例如，用于下游任务），请使用</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;.create_documents&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">。</font></p>
<pre><code class="language-python">%pip install -qU langchain-text-splitters</code></pre>
<pre><code class="language-python">#示例：recursively_split.py
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 加载示例文档
with open("../../resource/knowledge.txt", encoding="utf-8") as f:
    state_of_the_union = f.read()
text_splitter = RecursiveCharacterTextSplitter(
    # 设置一个非常小的块大小，只是为了展示。
    chunk_size=100,
    chunk_overlap=20,
    length_function=len,
    is_separator_regex=False,
)
texts = text_splitter.create_documents([state_of_the_union])
print(texts[0])
print(texts[1])</code></pre>
<pre><code class="language-plain">page_content='﻿I am honored to be with you today at your commencement from one of the finest universities in the'
page_content='universities in the world. I never graduated from college. Truth be told, this is the closest I've'</code></pre>
<pre><code class="language-python">text_splitter.split_text(knowledge)[:2]</code></pre>
<pre><code class="language-python">['\ufeffI am honored to be with you today at your commencement from one of the finest universities in the', "universities in the world. I never graduated from college. Truth be told, this is the closest I've"]</code></pre>
<p><font style="color:rgb(28, 30, 33);">让我们来看看上述</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;RecursiveCharacterTextSplitter&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">的参数设置：</font></p>
<ul>
<li><code>&lt;font style="color:rgb(28, 30, 33);"&gt;chunk_size&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">：块的最大大小，大小由</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;length_function&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">决定。</font></li>
<li><code>&lt;font style="color:rgb(28, 30, 33);"&gt;chunk_overlap&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">：块之间的目标重叠。重叠的块有助于在上下文分割时减少信息丢失。</font></li>
<li><code>&lt;font style="color:rgb(28, 30, 33);"&gt;length_function&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">：确定块大小的函数。</font></li>
<li><code>&lt;font style="color:rgb(28, 30, 33);"&gt;is_separator_regex&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">：分隔符列表（默认为</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;["\n\n", "\n", " ", ""]&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">）是否应被解释为正则表达式。</font></li>
</ul>
<h4>从没有词边界的语言中分割文本</h4>
<p><font style="color:rgb(28, 30, 33);">一些书写系统没有</font><a href="https://en.wikipedia.org/wiki/Category:Writing_systems_without_word_boundaries"><font style="color:rgb(28, 30, 33);">词边界</font></a><font style="color:rgb(28, 30, 33);">，例如中文、日文和泰文。使用默认分隔符列表</font><font style="color:rgb(28, 30, 33);"> </font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;["\n\n", "\n", " ", ""]&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">分割文本可能会导致单词被分割在不同块之间。为了保持单词在一起，您可以覆盖分隔符列表，包括额外的标点符号：</font></p>
<ul>
<li><font style="color:rgb(28, 30, 33);">添加 ASCII 句号 &quot;</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;.&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">&quot;，</font><a href="https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)"><font style="color:rgb(28, 30, 33);">Unicode 全角</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">句号 &quot;</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;．&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">&quot;（用于中文文本），以及</font><a href="https://en.wikipedia.org/wiki/CJK_Symbols_and_Punctuation"><font style="color:rgb(28, 30, 33);">表意句号</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">&quot;</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;。&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">&quot;（用于日文和中文）</font></li>
<li><font style="color:rgb(28, 30, 33);">添加</font><a href="https://en.wikipedia.org/wiki/Zero-width_space"><font style="color:rgb(28, 30, 33);">零宽空格</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">用于泰文、缅甸文、高棉文和日文。</font></li>
<li><font style="color:rgb(28, 30, 33);">添加 ASCII 逗号 &quot;</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;,&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">&quot;，Unicode 全角逗号 &quot;</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;，&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">&quot;，以及 Unicode 表意逗号 &quot;</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;、&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">&quot;</font></li>
</ul>
<pre><code class="language-python">#示例：recursively_separator.py
text_splitter = RecursiveCharacterTextSplitter(
    separators=[
        "\n\n",
        "\n",
        " ",
        ".",
        ",",
        "\u200b",  # 零宽空格
        "\uff0c",  # 全角逗号
        "\u3001",  # 表意逗号
        "\uff0e",  # 全角句号
        "\u3002",  # 表意句号
        "",
    ],
    # 已有的参数
)</code></pre>
<hr />
<h3>按照语义块分割文本</h3>
<p><font style="color:rgb(28, 30, 33);">下面介绍如何根据语义相似性拆分文本块(semantic chunks)。如果嵌入足够远，文本块将被拆分。</font></p>
<p><font style="color:rgb(28, 30, 33);">在高层次上，这将文本拆分成句子，然后分组为每组3个句子，最后合并在嵌入空间中相似的句子。</font></p>
<h4>安装依赖项</h4>
<pre><code class="language-python">#pip install --quiet langchain_experimental langchain_openai</code></pre>
<h4>载入示例数据</h4>
<pre><code class="language-python">#示例：semantic_split.py
# 这是一个长文档，我们可以将其拆分。
with open("../../resource/knowledge.txt", encoding="utf-8") as f:
    knowledge = f.read()</code></pre>
<h4>创建文本拆分器</h4>
<p><font style="color:rgb(28, 30, 33);">要实例化一个</font><a href="https://api.python.langchain.com/en/latest/text_splitter/langchain_experimental.text_splitter.SemanticChunker.html"><font style="color:rgb(28, 30, 33);">SemanticChunker</font></a><font style="color:rgb(28, 30, 33);">，我们必须指定一个嵌入模型。下面我们将使用</font><a href="https://api.python.langchain.com/en/latest/embeddings/langchain_community.embeddings.openai.OpenAIEmbeddings.html"><font style="color:rgb(28, 30, 33);">OpenAIEmbeddings</font></a><font style="color:rgb(28, 30, 33);">。</font></p>
<pre><code class="language-python">from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker(OpenAIEmbeddings())</code></pre>
<h4>拆分文本</h4>
<p><font style="color:rgb(28, 30, 33);">我们按照通常的方式拆分文本，例如，通过调用</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;.create_documents&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">来创建 LangChain</font><font style="color:rgb(28, 30, 33);"> </font><a href="https://api.python.langchain.com/en/latest/documents/langchain_core.documents.base.Document.html"><font style="color:rgb(28, 30, 33);">Document</font></a><font style="color:rgb(28, 30, 33);"> </font><font style="color:rgb(28, 30, 33);">对象：</font></p>
<pre><code class="language-python">docs = text_splitter.create_documents([knowledge])
print(docs[0].page_content)</code></pre>
<pre><code class="language-plain">I am honored to be with you today at your commencement from one of the finest universities in the world. I never graduated from college. Truth be told, this is the closest I've ever gotten to a college graduation. Today I want to tell you three stories from my life. That's it. No big deal.</code></pre>
<h4>断点</h4>
<p><font style="color:rgb(28, 30, 33);">这个拆分器的工作原理是确定何时“断开”句子。这是通过查找任意两个句子之间的嵌入差异来完成的。当该差异超过某个阈值时，它们就会被拆分。</font></p>
<p><font style="color:rgb(28, 30, 33);">有几种方法可以确定该阈值，这由</font><code>&lt;font style="color:rgb(28, 30, 33);"&gt;breakpoint_threshold_type&lt;/font&gt;</code><font style="color:rgb(28, 30, 33);">关键字参数控制。</font></p>
<h5>百分位数</h5>
<p><font style="color:rgb(28, 30, 33);">拆分的默认方式是基于百分位数。在此方法中，计算所有句子之间的差异，然后任何大于X百分位数的差异都会被拆分。</font></p>
<pre><code class="language-python">#示例：semantic_split_percentile.py
text_splitter = SemanticChunker(
    OpenAIEmbeddings(), breakpoint_threshold_type="percentile", breakpoint_threshold_amount=50
)</code></pre>
<pre><code class="language-python">docs = text_splitter.create_documents([knowledge])
print(docs[0].page_content)</code></pre>
<pre><code class="language-plain">I am honored to be with you today at your commencement from one of the finest universities in the world. I never graduated from college.</code></pre>
<pre><code class="language-python">print(len(docs))</code></pre>
<pre><code class="language-plain">71</code></pre>
<h1><font style="color:#000000;">Loader, Splitter, Embedding, Vector Store, Retrievers 的综合应用</font></h1>
<h2><font style="color:rgb(51, 51, 51);">使用 Streamlit 实现一个支持记忆的 RAG 问答 APP</font></h2>
<h3><font style="color:rgb(51, 51, 51);">实现流程</font></h3>
<p><font style="color:rgb(51, 51, 51);">一个 RAG 程序的 APP 主要有以下流程：</font></p>
<ol>
<li><font style="color:rgb(51, 51, 51);">用户在 RAG 客户端上传一个txt文件</font></li>
<li><font style="color:rgb(51, 51, 51);">服务器端接收客户端文件，存储在服务端</font></li>
<li><font style="color:rgb(51, 51, 51);">服务器端程序对文件进行读取</font></li>
<li><font style="color:rgb(51, 51, 51);">对文件内容进行拆分，防止一次性塞给 Embedding 模型超 token 限制</font></li>
<li><font style="color:rgb(51, 51, 51);">把 Embedding 后的内容存储在向量数据库，生成检索器</font></li>
<li><font style="color:rgb(51, 51, 51);">程序准备就绪，允许用户进行提问</font></li>
<li><font style="color:rgb(51, 51, 51);">用户提出问题，大模型调用检索器检索文档，把相关片段找出来后，组织后，回复用户。</font></li>
</ol>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/tech/image-20250419151732642.png" alt="image-20250419151732642" /></p>
<h3><font style="color:rgb(51, 51, 51);">引入记忆</font></h3>
<p><font style="color:rgb(51, 51, 51);">以上流程实现起来没有太大难度，但是如果你想在这个基础上实现模型记忆功能，就会比较困难了。梳理下为什么引入记忆会存在困难。我们把引入模型记忆功能之后的使用场景演绎下：</font></p>
<p><font style="color:rgb(51, 51, 51);">假如我们上传了一份最新的新闻内容 txt 文件，如下：</font></p>
<pre><code class="language-plain">2024 世界人工智能大会暨人工智能全球治理高级别会议（简称“WAIC 2024”）将于 7 月4日-6日在上海世博中心、世博展览馆举行。
同时外交部消息称，重要领导人将于7月4日出席开幕式，并发表主旨演讲。
大会围绕“以共商促共享 以善治促善智（Governing AI for Good and for All）”主题，
聚焦大模型、算力、机器人、自动驾驶等重点领域，首发一批创新产品。
目前，已有500余家企业确认参展，市外企业和国际企业占比超50%，展品数量已超1500项，参展企业数 、亮点展品数和首发新品数均创历史最高，
进一步释放大会对人工智能产业的“磁场效应”。
市场普遍认为，AI产业链有望迎来新突破，行业配置价值凸显.</code></pre>
<p><font style="color:rgb(51, 51, 51);">如下是针对这份文档的提问：</font></p>
<pre><code class="language-python">用户：2024 世界人工智能大会那一天开始，地点在哪?
大模型：2024 世界人工智能大会暨人工智能全球治理高级别会议将于7月4日至6日在上海世博中心、世博展览馆举行。
用户：大会主题是什么?
大模型：大会主题是"以共商促共享 以善治促善智（Governing AI for Good and for All）"。</code></pre>
<p><font style="color:rgb(51, 51, 51);">用户的第二次提问是针对大模型的回复上下文场景下的，所以大模型第二次提问，需要根据整个对话内容重新理解提问，并且改写用户的提问，再通过检索器进行检索后，最终回答用户的第二次提问。</font></p>
<p><font style="color:rgb(51, 51, 51);">所以引入记忆带来的额外问题的核心就是，如果需要 RAG 程序要支持记忆，就需要额外加入模型理解对话上下文后改写问题，再次进行检索并回答的流程。</font></p>
<p><font style="color:rgb(51, 51, 51);">整个流程需要改成如下：</font></p>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/tech/image-20250419151803490.png" alt="image-20250419151803490" /></p>
<h3><font style="color:rgb(51, 51, 51);">代码实现</font></h3>
<p><font style="color:rgb(51, 51, 51);">使用 Streamlit 实现文件上传，我这里只实现了 txt 文件上传，其实这里可以在 </font><a href="https://so.csdn.net/so/search?q=type&amp;spm=1001.2101.3001.7020"><font style="color:rgb(51, 51, 51);">type</font></a><font style="color:rgb(51, 51, 51);"> 参数里面设置多个文件类型，在后面的检索器方法里面针对每个类型进行处理即可。</font></p>
<h4><font style="color:rgb(51, 51, 51);">实现文件上传</font></h4>
<pre><code class="language-python">#示例：txt_search.py
import streamlit as st

# 上传txt文件，允许上传多个文件
uploaded_files = st.sidebar.file_uploader(
    label="上传txt文件", type=["txt"], accept_multiple_files=True
)
if not uploaded_files:
    st.info("请先上传按TXT文档。")
    st.stop()</code></pre>
<p><font style="color:rgb(51, 51, 51);">stremlit run 一下，看下效果</font></p>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/tech/image-20250419151834875.png" alt="image-20250419151834875" /></p>
<h4><font style="color:rgb(51, 51, 51);">实现检索器</font></h4>
<p><font style="color:rgb(51, 51, 51);">注意 chunk_size 最大设置数值取决于 Embedding 模型允许单词的最大字符数限制。</font></p>
<pre><code class="language-python">import tempfile
import os
from langchain.document_loaders import TextLoader
from langchain_community.embeddings import QianfanEmbeddingsEndpoint
from langchain_chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 实现检索器
@st.cache_resource(ttl="1h")
def configure_retriever(uploaded_files):
    # 读取上传的文档，并写入一个临时目录
    docs = []
    temp_dir = tempfile.TemporaryDirectory(dir=r"D:\tmp")
    for file in uploaded_files:
        temp_filepath = os.path.join(temp_dir.name, file.name)
        with open(temp_filepath, "wb") as f:
            f.write(file.getvalue())
        loader = TextLoader(temp_filepath, encoding="utf-8")
        docs.extend(loader.load())

    # 进行文档分割
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=30)
    splits = text_splitter.split_documents(docs)

    # 这里使用了OpenAI向量模型
    embeddings = OpenAIEmbeddings()
    vectordb = Chroma.from_documents(splits, embeddings)

    retriever = vectordb.as_retriever()

    return retriever

retriever = configure_retriever(uploaded_files)</code></pre>
<h4><font style="color:rgb(51, 51, 51);">创建检索工具</font></h4>
<p><font style="color:rgb(51, 51, 51);">langchain 提供了 create_retriever_tool 工具，可以直接用。</font></p>
<pre><code class="language-python"># 创建检索工具
from langchain.tools.retriever import create_retriever_tool

tool = create_retriever_tool(
    retriever,
    "文档检索",
    "用于检索用户提出的问题，并基于检索到的文档内容进行回复.",
)
tools = [tool]</code></pre>
<h4><font style="color:rgb(51, 51, 51);">创建 React Agent</font></h4>
<pre><code class="language-python">instructions = """您是一个设计用于查询文档来回答问题的代理。
您可以使用文档检索工具，并基于检索内容来回答问题
您可能不查询文档就知道答案，但是您仍然应该查询文档来获得答案。
如果您从文档中找不到任何信息用于回答问题，则只需返回“抱歉，这个问题我还不知道。”作为答案。
"""

base_prompt_template = """
{instructions}

TOOLS:
------

You have access to the following tools:

{tools}

To use a tool, please use the following format:

•```
Thought: Do I need to use a tool? Yes
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
•```

When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:

•```
Thought: Do I need to use a tool? No
Final Answer: [your response here]
•```

Begin!

Previous conversation history:
{chat_history}

New input: {input}
{agent_scratchpad}"""

base_prompt = PromptTemplate.from_template(base_prompt_template)

prompt = base_prompt.partial(instructions=instructions)

# 创建llm
llm = ChatOpenAI()

# 创建react Agent
agent = create_react_agent(llm, tools, prompt)

agent_executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=False)</code></pre>
<h4><font style="color:rgb(51, 51, 51);">实现 Agent 回复</font></h4>
<p><font style="color:rgb(51, 51, 51);">获取用户输入，并回复用户，这里使用 StreamlitCallbackHandler 实现了 React 推理回调，可以让模型的推理过程可见。</font></p>
<pre><code class="language-python"># 创建聊天输入框
user_query = st.chat_input(placeholder="请开始提问吧!")

if user_query:
    st.session_state.messages.append({"role": "user", "content": user_query})
    st.chat_message("user").write(user_query)

    with st.chat_message("assistant"):
        st_cb = StreamlitCallbackHandler(st.container())
        config = {"callbacks": [st_cb]}
        response = agent_executor.invoke({"input": user_query}, config=config)
        st.session_state.messages.append({"role": "assistant", "content": response["output"]})
        st.write(response["output"])</code></pre>
<p><font style="color:rgb(51, 51, 51);">‍</font></p>
<h4><font style="color:rgb(51, 51, 51);">完整代码</font></h4>
<pre><code class="language-python">#示例：txt_search.py
#pip install streamlit==1.37.0
import streamlit as st
import tempfile
import os
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import StreamlitChatMessageHistory
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import PromptTemplate
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.agents import create_react_agent, AgentExecutor
from langchain_community.callbacks.streamlit import StreamlitCallbackHandler
from langchain_openai import ChatOpenAI

# 设置Streamlit应用的页面标题和布局
st.set_page_config(page_title="Rag Agent", layout="wide")
# 设置应用的标题
st.title("Rag Agent")

# 上传txt文件，允许上传多个文件
uploaded_files = st.sidebar.file_uploader(
    label="上传txt文件", type=["txt"], accept_multiple_files=True
)
# 如果没有上传文件，提示用户上传文件并停止运行
if not uploaded_files:
    st.info("请先上传按TXT文档。")
    st.stop()

# 实现检索器
@st.cache_resource(ttl="1h")
def configure_retriever(uploaded_files):
    # 读取上传的文档，并写入一个临时目录
    docs = []
    temp_dir = tempfile.TemporaryDirectory(dir=r"D:\tmp")
    for file in uploaded_files:
        temp_filepath = os.path.join(temp_dir.name, file.name)
        with open(temp_filepath, "wb") as f:
            f.write(file.getvalue())
        # 使用TextLoader加载文本文件
        loader = TextLoader(temp_filepath, encoding="utf-8")
        docs.extend(loader.load())

    # 进行文档分割
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50)
    splits = text_splitter.split_documents(docs)

    # 使用OpenAI的向量模型生成文档的向量表示
    embeddings = OpenAIEmbeddings()
    vectordb = Chroma.from_documents(splits, embeddings)

    # 创建文档检索器
    retriever = vectordb.as_retriever()

    return retriever

# 配置检索器
retriever = configure_retriever(uploaded_files)

# 如果session_state中没有消息记录或用户点击了清空聊天记录按钮，则初始化消息记录
if "messages" not in st.session_state or st.sidebar.button("清空聊天记录"):
    st.session_state["messages"] = [{"role": "assistant", "content": "您好，我是聚客AI助手，我可以查询文档"}]

# 加载历史聊天记录
for msg in st.session_state.messages:
    st.chat_message(msg["role"]).write(msg["content"])

# 创建检索工具
from langchain.tools.retriever import create_retriever_tool

# 创建用于文档检索的工具
tool = create_retriever_tool(
    retriever,
    "文档检索",
    "用于检索用户提出的问题，并基于检索到的文档内容进行回复.",
)
tools = [tool]

# 创建聊天消息历史记录
msgs = StreamlitChatMessageHistory()
# 创建对话缓冲区内存
memory = ConversationBufferMemory(
    chat_memory=msgs, return_messages=True, memory_key="chat_history", output_key="output"
)

# 指令模板
instructions = """您是一个设计用于查询文档来回答问题的代理。
您可以使用文档检索工具，并基于检索内容来回答问题
您可能不查询文档就知道答案，但是您仍然应该查询文档来获得答案。
如果您从文档中找不到任何信息用于回答问题，则只需返回“抱歉，这个问题我还不知道。”作为答案。
"""

# 基础提示模板
base_prompt_template = """
{instructions}

TOOLS:
------

You have access to the following tools:

{tools}

To use a tool, please use the following format:

‍```
Thought: Do I need to use a tool? Yes
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
‍```

When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:

‍```
Thought: Do I need to use a tool? No
Final Answer: [your response here]
‍```

Begin!

Previous conversation history:
{chat_history}

New input: {input}
{agent_scratchpad}"""

# 创建基础提示模板
base_prompt = PromptTemplate.from_template(base_prompt_template)

# 创建部分填充的提示模板
prompt = base_prompt.partial(instructions=instructions)

# 创建llm
llm = ChatOpenAI()

# 创建react Agent
agent = create_react_agent(llm, tools, prompt)

# 创建Agent执行器
agent_executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=True, handle_parsing_errors=True)

# 创建聊天输入框
user_query = st.chat_input(placeholder="请开始提问吧!")

# 如果有用户输入的查询
if user_query:
    # 添加用户消息到session_state
    st.session_state.messages.append({"role": "user", "content": user_query})
    # 显示用户消息
    st.chat_message("user").write(user_query)

    with st.chat_message("assistant"):
        # 创建Streamlit回调处理器
        st_cb = StreamlitCallbackHandler(st.container())
        # agent执行过程日志回调显示在Streamlit Container (如思考、选择工具、执行查询、观察结果等)
        config = {"callbacks": [st_cb]}
        # 执行Agent并获取响应
        response = agent_executor.invoke({"input": user_query}, config=config)
        # 添加助手消息到session_state
        st.session_state.messages.append({"role": "assistant", "content": response["output"]})
        # 显示助手响应
        st.write(response["output"])</code></pre>
<h4><font style="color:rgb(51, 51, 51);">实现效果</font></h4>
<p><font style="color:rgb(51, 51, 51);">上传 </font><font style="color:#080808;background-color:#ffffff;">threebody.txt</font><font style="color:rgb(51, 51, 51);"> 内容：</font></p>
<pre><code class="language-plain">在《三体》系列中，执剑人是指掌握着地球与三体文明之间威慑平衡的关键人物。执剑人负责控制一种名为“引力波发射器”的装置，这个装置能够向宇宙广播地球和三体星系的坐标，从而触发宇宙中的黑暗森林法则，导致三体文明和地球文明同时被其他文明消灭。因此，执剑人实际上是维持地球与三体之间脆弱和平的关键。
第一任执剑人罗辑之所以让三体人感到害怕，是因为他通过与三体文明的接触，逐渐理解了三体文明的弱点和宇宙中的黑暗森林法则。罗辑利用这些知识，采取了一系列策略，使得三体文明无法轻易地消灭人类，甚至可能对人类文明构成威胁。
罗辑通过与三体文明的对话，发现了三体文明对地球的入侵计划，并利用自己的智慧和策略，成功地将地球文明置于一个相对安全的位置。他通过与三体文明的博弈，展示了人类的智慧和坚韧，使得三体文明意识到人类并非易于征服的对手。
此外，罗辑还揭示了宇宙中文明的生存状态，即所谓的“黑暗森林法则”，这个法则认为宇宙中的文明如同森林中的猎人，每个文明都在暗中观察其他文明，一旦发现潜在的威胁，就会先发制人。罗辑利用这一法则，使得三体文明不敢轻易对人类采取极端行动，因为他们担心会引来其他文明的注意和攻击。
因此，罗辑的智慧、策略以及对宇宙法则的理解，使得三体人对他感到害怕，不敢轻视人类的力量和潜力。
第二任执剑人是程心，程心在小说中接替了罗辑成为执剑人，负责控制引力波发射器，维持地球与三体文明之间的威慑平衡。当执剑人更换时，三体文明认为新执剑人可能不具备前任执剑人的威慑力和决断力，或者无法像前任那样坚定地执行威慑策略。这种不确定性使得三体文明认为发动攻击的风险降低了，因此他们决定抓住机会，发起了对地球的攻击。</code></pre>
<p><font style="color:rgb(51, 51, 51);">基于上述内容进行提问，效果如下：</font></p>
<pre><code class="language-plain">用户：执剑人是什么角色
大模型：在《三体》系列中，执剑人是掌握着地球与三体文明之间威慑平衡的关键人物，负责控制引力波发射器，维持地球与三体之间的平衡。第一任执剑人是罗辑，第二任为程心。罗辑通过智慧和策略，成功维护地球文明。

用户：他通过什么办法威胁三体文明
大模型：罗辑通过与三体文明的对话和博弈，利用自己的智慧、坚韧和对宇宙法则的理解，成功威胁三体文明，使他们不敢轻易对地球采取极端行动。程心则在接替罗辑成为执剑人后，由于三体文明对新执剑人的不确定性，导致他们认为发动攻击的风险降低了，从而决定发起对地球的攻击。</code></pre>
<p><img src="https://raw.githubusercontent.com/zhaomo08/Photo_Notes/main/tech/image-20250420223658267.png" alt="image-20250420223658267" /></p>]]></description>
    <pubDate>Sat, 19 Apr 2025 15:19:54 +0800</pubDate>
    <dc:creator>Chester</dc:creator>
    <guid>https://blog.sayyou.icu/?post=19</guid>
</item>
</channel>
</rss>