<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>孤独终洁</title><description>写文字、记生活，在网络的角落里留下成长与经历的痕迹——不再只是孤独，而是与自己对话。</description><link>https://blog.teclado.cn/</link><item><title>Dify 生产级私有化部署实践</title><link>https://blog.teclado.cn/posts/dify-production-deployment/dify-%E7%94%9F%E4%BA%A7%E7%BA%A7%E7%A7%81%E6%9C%89%E5%8C%96%E9%83%A8%E7%BD%B2%E5%AE%9E%E8%B7%B5/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/dify-production-deployment/dify-%E7%94%9F%E4%BA%A7%E7%BA%A7%E7%A7%81%E6%9C%89%E5%8C%96%E9%83%A8%E7%BD%B2%E5%AE%9E%E8%B7%B5/</guid><pubDate>Sat, 20 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;一份从踩坑到落地的生产级 Dify 私有化部署经验总结，帮助你绕开官方一键部署到生产环境之间的那道鸿沟。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;为什么不直接用官方 docker-compose&lt;/h2&gt;
&lt;p&gt;最近把 Dify 真正放到生产环境上跑了一遍，过程比预想要曲折。官方仓库里那份 &lt;code&gt;docker/docker-compose.yaml&lt;/code&gt; 一行命令就能拉起完整一套服务，看上去很方便，但实际用过会发现它的定位是「快速体验」，离能承载业务的生产环境差得比较远。&lt;/p&gt;
&lt;p&gt;差别主要在这几件事上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有状态组件（PostgreSQL、Redis、向量库）和无状态组件全部堆在一份 compose 里，升级、备份、扩容都会互相牵扯；&lt;/li&gt;
&lt;li&gt;单机部署，没法水平扩展；&lt;/li&gt;
&lt;li&gt;默认 &lt;code&gt;.env&lt;/code&gt; 接近 400 行，真出问题时不知道从哪里下手；&lt;/li&gt;
&lt;li&gt;数据都压在本地卷上，机器一挂全没。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我自己折腾完之后，把整个过程整理成这一篇，包括架构怎么拆、组件怎么选、踩过哪些坑，以及一份填完坑之后可以直接改值就用的 &lt;code&gt;.env&lt;/code&gt; 和 &lt;code&gt;docker-compose.prod.yaml&lt;/code&gt;。希望能让做类似事情的人省掉一些反复试错的时间。&lt;/p&gt;
&lt;h2&gt;架构拆分的思路&lt;/h2&gt;
&lt;p&gt;整套架构里我最看重的判断其实只有一个：&lt;strong&gt;把有状态和无状态彻底分开。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有状态组件——PostgreSQL、Redis、Qdrant——丢一次数据就回不来了，必须独立部署、独立备份、独立监控。无状态组件——API、Worker、Web、Sandbox、Plugin Daemon——挂了重启就行，可以放心扩缩容。这个边界划清楚之后，后续的所有事都顺了：应用层挂了不会丢数据，数据层升配不会影响应用，要水平扩展前面套个 SLB 再复制一台 ECS 就行。&lt;/p&gt;
&lt;p&gt;最终落到这样一张架构图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./architecture-gpt.png&quot; alt=&quot;Dify 生产级私有化部署架构图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;几个具体的设计点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有阿里云资源放在同一个 VPC 内走内网通信，延迟低、流量基本不计费，也更安全；&lt;/li&gt;
&lt;li&gt;初期一台 ECS 跑应用层完全够用，需要扩容再前置 SLB；&lt;/li&gt;
&lt;li&gt;只有 SMTP 走外网，如果用阿里云邮件推送 DM 还可以改成内网；&lt;/li&gt;
&lt;li&gt;备份策略只需要盯 RDS、Redis（持久化）、OSS 三个地方，简单清晰。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;组件选型最后是这样的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;选型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;关系型数据库&lt;/td&gt;
&lt;td&gt;阿里云 RDS PostgreSQL&lt;/td&gt;
&lt;td&gt;托管服务，自动备份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存&lt;/td&gt;
&lt;td&gt;阿里云 Redis&lt;/td&gt;
&lt;td&gt;托管服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向量数据库&lt;/td&gt;
&lt;td&gt;独立 ECS + Qdrant Docker&lt;/td&gt;
&lt;td&gt;成本与性能平衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;应用层&lt;/td&gt;
&lt;td&gt;ECS + docker-compose&lt;/td&gt;
&lt;td&gt;无状态，便于扩展&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对象存储&lt;/td&gt;
&lt;td&gt;阿里云 OSS&lt;/td&gt;
&lt;td&gt;文件 / 知识库存储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邮件&lt;/td&gt;
&lt;td&gt;SMTP 服务&lt;/td&gt;
&lt;td&gt;用户激活 / 通知&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;域名&lt;/td&gt;
&lt;td&gt;备案域名 + 二级域名&lt;/td&gt;
&lt;td&gt;如 dify.example.com&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;向量库这一项我犹豫了一下。阿里云有托管向量服务，但小规模阶段性价比偏低，而 Dify 对 Qdrant 的支持很成熟，所以最后还是自建了一台 ECS。后续数据量上来要切到托管或者 Qdrant 集群都不算难。&lt;/p&gt;
&lt;h2&gt;前期资源准备&lt;/h2&gt;
&lt;p&gt;下面这些资源建议提前一次性都准备好，部署的时候才不会卡在某个白名单或者证书上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;区域规划。&lt;/strong&gt; 所有资源必须选在同一个地域和同一个可用区。这点一开始很容易忽略，等部署到一半才发现 ECS 在杭州、RDS 在上海，就只能拆了重来。同地域同可用区可以走 VPC 内网通信，速度快、流量不计费，安全边界也更清晰。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;阿里云 RDS PostgreSQL。&lt;/strong&gt; 实例规格前期不用买太高，2 核 4G 入门规格就够先跑起来，后续升配很方便。&lt;/p&gt;
&lt;p&gt;版本必须选 15。阿里云控制台默认会推荐 18，但 Dify 的 migration 脚本里自定义了一个 &lt;code&gt;uuidv7()&lt;/code&gt; 函数，而 PG 18 原生就提供了同名函数，撞名之后迁移直接挂掉。RDS 不支持降版本，选错了只能重建实例。&lt;/p&gt;
&lt;p&gt;数据库初始化时准备两个 database 和一个专用账号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dify
dify_plugin

用户名：dify_user
权限：dify 和 dify_plugin 两个库的读写 / 建表权限
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 Dify ECS 的内网 IP 加进 RDS 白名单，再从 ECS 用一个临时容器测连通性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm -it postgres:15-alpine psql \
  &quot;postgresql://dify_user:&amp;lt;RDS密码&amp;gt;@&amp;lt;RDS内网地址&amp;gt;:5432/postgres&quot; \
  -c &quot;select version();&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回版本号就说明 ECS 到 RDS 通了，再确认一下版本是 15。顺带提一句，连接串里如果密码包含 &lt;code&gt;!&lt;/code&gt;、&lt;code&gt;@&lt;/code&gt;、&lt;code&gt;#&lt;/code&gt;、&lt;code&gt;%&lt;/code&gt;、&lt;code&gt;/&lt;/code&gt; 这类特殊字符，必须 URL 编码（比如 &lt;code&gt;!&lt;/code&gt; 写成 &lt;code&gt;%21&lt;/code&gt;），否则解析会出错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;阿里云 Redis。&lt;/strong&gt; 主要扛缓存和 Celery 队列，前期 1G 入门版够用。创建之后记得设置访问密码，然后开 VPC 白名单。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm -it redis:7-alpine redis-cli \
  -h &amp;lt;Redis内网地址&amp;gt; -p 6379 -a &apos;&amp;lt;Redis密码&amp;gt;&apos; ping
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回 &lt;code&gt;PONG&lt;/code&gt; 就 OK。Redis DB 分配建议 &lt;code&gt;REDIS_DB=0&lt;/code&gt; 给 Dify 自身缓存，&lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; 走 &lt;code&gt;/1&lt;/code&gt; 给 Celery 队列。Dify 官方文档也明确提醒这两边不要冲突——Celery 默认 broker 格式就是 &lt;code&gt;redis://.../&amp;lt;redis_database&amp;gt;&lt;/code&gt;，分开能避免互相干扰。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;独立部署 Qdrant。&lt;/strong&gt; 向量库我单独开了一台 ECS 跑 Qdrant 容器，规格重点关注内存和磁盘：内存决定能加载多少索引，磁盘决定能存多少文档块，2 核 4G + 100G SSD 起步比较舒服。&lt;/p&gt;
&lt;p&gt;步骤大致是：装好 Docker → &lt;code&gt;docker pull qdrant/qdrant&lt;/code&gt; → 准备一份 &lt;code&gt;config/config.yaml&lt;/code&gt; 配置持久化路径和 &lt;code&gt;api-key&lt;/code&gt;（一定要开 api-key，不开就是裸奔）→ 启动容器并挂载数据盘 → 安全组只对 VPC 内网开放 &lt;code&gt;6333&lt;/code&gt; 和 &lt;code&gt;6334&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从 Dify ECS 测一下连通性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s http://&amp;lt;Qdrant内网IP&amp;gt;:6333/collections \
  -H &quot;api-key: &amp;lt;Qdrant_API_KEY&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回 &lt;code&gt;{&quot;result&quot;:{&quot;collections&quot;:[]},&quot;status&quot;:&quot;ok&quot;,...}&lt;/code&gt; 就说明通了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其他基础设施。&lt;/strong&gt; 剩下几件零碎但少不了的：备案过的域名 + 二级域名（例如 &lt;code&gt;dify.example.com&lt;/code&gt;）；SMTP 邮箱（这里以阿里云企业邮为例）；阿里云 OSS Bucket 和 AccessKey；SSL 证书（阿里云免费 DV 就够，下载 Nginx 版本备用）。&lt;/p&gt;
&lt;h2&gt;部署应用层&lt;/h2&gt;
&lt;p&gt;应用层 ECS 我建议至少 4 核 8G + 100G 系统盘。Plugin Daemon 装插件时会用 &lt;code&gt;uv&lt;/code&gt; 构建虚拟环境，内存太小很容易被 OOM Killer 杀掉。&lt;/p&gt;
&lt;p&gt;环境准备很简单：装好 Docker 和 docker compose（版本 ≥ 2.20），给应用建一个独立目录，比如 &lt;code&gt;/opt/dify-prod&lt;/code&gt;。源码层面我们不需要编译，只用到 &lt;code&gt;docker/&lt;/code&gt; 目录下的 Nginx 模板、Sandbox 配置、SSRF Proxy 配置这些静态资源。国内访问 GitHub 不太稳定，从 Gitee 镜像拉就行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://gitee.com/dify_ai/dify.git
cd dify
git checkout &amp;lt;稳定版本 tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选 tag 时挑一个已经发布一段时间、社区反馈稳定的版本，不用追最新。我这次用的是 &lt;code&gt;1.14.2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;官方那份 &lt;code&gt;docker-compose.yaml&lt;/code&gt; 我没有直接复用，原因有几个：它把所有可选向量库（Weaviate、Qdrant、Milvus、PGVector 等）的服务定义都堆在一起靠 profile 切换；配置项又多又乱；并且包含了我们已经独立部署的组件（PG、Redis、Qdrant），后续维护起来太重。所以我重新写了一份精简版，只保留真正要跑的无状态组件（api、worker、worker_beat、web、sandbox、plugin_daemon、ssrf_proxy、nginx），并且在底部显式指定了 Docker 网段——这块和踩坑相关，下面会单独讲。&lt;/p&gt;
&lt;p&gt;整套部署最关键的就是两份文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完整的 &lt;a href=&quot;/files/dify/docker-compose.prod.yaml&quot;&gt;docker-compose.prod.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;完整的 &lt;a href=&quot;/files/dify/.env.example&quot;&gt;.env.example&lt;/a&gt;（所有敏感信息已替换为占位符）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;compose 文件基本拿过去改个版本号就能用，没有需要逐项理解的地方。&lt;code&gt;.env&lt;/code&gt; 配置块比较多，下面挑几个&lt;strong&gt;必须根据自己环境改值、并且容易出错&lt;/strong&gt;的关键块单独说一下，其余的部分（Sandbox、Plugin Daemon、Nginx、SSRF Proxy 等）保持模板默认即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL（RDS）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DB_TYPE=postgresql
DB_USERNAME=dify_user
DB_PASSWORD=&amp;lt;RDS 数据库密码&amp;gt;
DB_HOST=&amp;lt;RDS 内网地址，例如 pgm-xxxxxxxx.pg.rds.aliyuncs.com&amp;gt;
DB_PORT=5432
DB_DATABASE=dify
DB_PLUGIN_DATABASE=dify_plugin
DB_SSL_MODE=disable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DB_DATABASE&lt;/code&gt; 是 Dify 主库，&lt;code&gt;DB_PLUGIN_DATABASE&lt;/code&gt; 是 plugin_daemon 单独使用的库，这两个库要提前在 RDS 里建好。密码里如果有特殊字符不用在这里编码，但 &lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; 那一行的密码需要 URL 编码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REDIS_HOST=&amp;lt;Redis 内网地址&amp;gt;
REDIS_PASSWORD=&amp;lt;Redis 密码&amp;gt;
REDIS_DB=0
CELERY_BROKER_URL=redis://:&amp;lt;URL编码后的Redis密码&amp;gt;@&amp;lt;Redis内网地址&amp;gt;:6379/1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意两点：&lt;code&gt;REDIS_DB=0&lt;/code&gt; 给 Dify 自身缓存，&lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; 走 &lt;code&gt;/1&lt;/code&gt; 给 Celery 队列，分开避免互相干扰；&lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; 里的密码必须 URL 编码（&lt;code&gt;!&lt;/code&gt; 写成 &lt;code&gt;%21&lt;/code&gt; 这种）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;向量库（Qdrant）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VECTOR_STORE=qdrant
VECTOR_INDEX_NAME_PREFIX=prod_
QDRANT_URL=http://&amp;lt;Qdrant ECS 内网 IP&amp;gt;:6333
QDRANT_API_KEY=&amp;lt;Qdrant 的 api-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;VECTOR_INDEX_NAME_PREFIX&lt;/code&gt; 用来区分环境（比如开发环境前缀写 &lt;code&gt;dev_&lt;/code&gt;），后面如果同一台 Qdrant 同时被多个环境用，靠这个前缀隔离 collection。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件存储（OSS）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;STORAGE_TYPE=aliyun-oss
ALIYUN_OSS_BUCKET_NAME=&amp;lt;你的 OSS Bucket 名&amp;gt;
ALIYUN_OSS_ACCESS_KEY=&amp;lt;AccessKey ID&amp;gt;
ALIYUN_OSS_SECRET_KEY=&amp;lt;AccessKey Secret&amp;gt;
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou-internal.aliyuncs.com
ALIYUN_OSS_REGION=cn-hangzhou
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ALIYUN_OSS_ENDPOINT&lt;/code&gt; 一定要用 &lt;code&gt;internal&lt;/code&gt; 内网域名，走外网不仅慢，还会被计算公网流量费。&lt;code&gt;.env.example&lt;/code&gt; 里 &lt;code&gt;PLUGIN_*&lt;/code&gt; 开头那一组配置实际上是把 plugin_daemon 的存储也指向了同一个 OSS Bucket，避免本地盘被插件文件撑满。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对外访问地址&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONSOLE_API_URL=https://dify.example.com
CONSOLE_WEB_URL=https://dify.example.com
SERVICE_API_URL=https://dify.example.com
APP_API_URL=https://dify.example.com
APP_WEB_URL=https://dify.example.com
FILES_URL=https://dify.example.com
TRIGGER_URL=https://dify.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一串全部填你的对外域名。没配 HTTPS 之前可以先写 &lt;code&gt;http://&lt;/code&gt;，证书装好后统一改成 &lt;code&gt;https://&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SMTP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAIL_TYPE=smtp
MAIL_DEFAULT_SEND_FROM=Dify &amp;lt;no-reply@example.com&amp;gt;
SMTP_SERVER=smtp.qiye.aliyun.com
SMTP_PORT=465
SMTP_USERNAME=no-reply@example.com
SMTP_PASSWORD=&amp;lt;SMTP 授权密码&amp;gt;
SMTP_USE_TLS=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SMTP_PASSWORD&lt;/code&gt; 是邮箱客户端的「授权密码」，不是登录密码，阿里云企业邮在邮箱设置里单独生成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;密钥与初始密码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;部署前需要准备两类凭据：随机密钥和服务间通信密钥，以及首次登录用的管理员初始密码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SECRET_KEY&lt;/code&gt; 是 Dify 签名 session 和 token 用的主密钥，用 &lt;code&gt;openssl rand -base64 42&lt;/code&gt; 生成即可，不要和其他密钥复用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;INIT_PASSWORD&lt;/code&gt; 是首次启动时初始化管理员账号的密码。注意不要超过 20 位——太长会导致初始化登录报错、无法进入系统。第一次用该密码登录成功后，请及时修改管理员密码。&lt;/p&gt;
&lt;p&gt;Sandbox 和 Plugin Daemon 各自还有一对服务间通信密钥，同样用 &lt;code&gt;openssl rand -base64 42&lt;/code&gt; 生成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SANDBOX_API_KEY&lt;/code&gt; 与 &lt;code&gt;CODE_EXECUTION_API_KEY&lt;/code&gt; 必须填同一个值；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PLUGIN_DAEMON_KEY&lt;/code&gt; 与 &lt;code&gt;PLUGIN_DIFY_INNER_API_KEY&lt;/code&gt; 建议保持一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;启动与验证&lt;/h2&gt;
&lt;p&gt;两个文件准备好，再把官方仓库 &lt;code&gt;docker/&lt;/code&gt; 下的 &lt;code&gt;nginx/&lt;/code&gt;、&lt;code&gt;ssrf_proxy/&lt;/code&gt;、&lt;code&gt;volumes/&lt;/code&gt; 这些目录拷过来，就可以启动了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose -f docker-compose.prod.yaml up -d
docker compose -f docker-compose.prod.yaml ps
docker compose -f docker-compose.prod.yaml logs -f api worker plugin_daemon
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动完成之后我习惯按这一份清单走一遍闭环测试，每一步对应一条关键链路，能跑通基本就说明这块没问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器访问域名，能进登录页 → Nginx + Web；&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;INIT_PASSWORD&lt;/code&gt; 登录 → API + RDS；&lt;/li&gt;
&lt;li&gt;创建一个模型供应商配置 → API；&lt;/li&gt;
&lt;li&gt;安装一个插件 → Plugin Daemon + PyPI 镜像 + OSS；&lt;/li&gt;
&lt;li&gt;上传一个小文件 → OSS；&lt;/li&gt;
&lt;li&gt;创建知识库并执行一次文档索引 → Qdrant 写入；&lt;/li&gt;
&lt;li&gt;创建一个简单工作流并运行 → Sandbox。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果某一步报错，日志命令里加上对应服务名定向看就行，不用一开始就 &lt;code&gt;logs -f&lt;/code&gt; 全量盯。&lt;/p&gt;
&lt;h2&gt;踩过的几个坑&lt;/h2&gt;
&lt;p&gt;下面这几个坑是我自己踩过来花了不少时间才搞清楚的，单独拎出来记一下。&lt;/p&gt;
&lt;h3&gt;插件安装报 &lt;code&gt;signal: killed&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;部署完登录都正常，但只要点安装插件就报：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;failed to launch plugin: failed to install dependencies:
failed to install dependencies: signal: killed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;plugin_daemon&lt;/code&gt; 在用 &lt;code&gt;uv&lt;/code&gt; 给插件下载依赖时被进程杀掉了。常见原因两个：内存不足被 OOM Killer 杀，或者 PyPI 下载太慢直接超时被杀。我这台 ECS 内存很健康，排除前者，剩下就是默认走国外 PyPI 太慢。&lt;/p&gt;
&lt;p&gt;解决办法是在 &lt;code&gt;.env&lt;/code&gt; 里加这三行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PIP_MIRROR_URL=https://mirrors.aliyun.com/pypi/simple/
PLUGIN_IGNORE_UV_LOCK=true
PYTHON_COMPILE_ALL_EXTRA_ARGS=-x \.venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PIP_MIRROR_URL&lt;/code&gt; 让依赖走阿里云镜像；&lt;code&gt;PLUGIN_IGNORE_UV_LOCK=true&lt;/code&gt; 让 &lt;code&gt;uv&lt;/code&gt; 不死盯插件原始 lock 文件，配合镜像源重新解析依赖；&lt;code&gt;-x \.venv&lt;/code&gt; 跳过对虚拟环境的字节码预编译，能明显降低初始化时的 CPU 消耗。这三个参数 Dify plugin-daemon 官方 README 都有推荐，但默认 &lt;code&gt;.env.example&lt;/code&gt; 不会带上，第一次部署很容易漏。&lt;/p&gt;
&lt;h3&gt;Redis 连接 &lt;code&gt;i/o timeout&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;API 启动一切正常，但只要碰 Redis 操作就报：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis.exceptions.ConnectionError: Error 113 connecting
dial tcp 172.18.25.239:6379: i/o timeout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;阿里云 Redis 的内网 IP 是 &lt;code&gt;172.18.25.239&lt;/code&gt;，而 Docker Compose 默认创建的 bridge 网络也经常落在 &lt;code&gt;172.18.0.0/16&lt;/code&gt; 这个段里。结果就是容器把 &lt;code&gt;172.18.25.239&lt;/code&gt; 当成 Docker 内部地址，根本不会把流量转出 VPC。&lt;/p&gt;
&lt;p&gt;解决办法是在 compose 里显式指定 Docker 网段，避开阿里云 VPC 常用的 &lt;code&gt;172.x.x.x&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;networks:
  default:
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.240.0/24
          gateway: 192.168.240.1
  ssrf_proxy_network:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 192.168.241.0/24
          gateway: 192.168.241.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是阿里云 + Docker 这套组合下非常经典的一个坑，部署前最好先看一眼 RDS、Redis 的内网 IP 段，再决定 Docker 用什么网段。&lt;/p&gt;
&lt;h3&gt;PostgreSQL 版本必须是 15&lt;/h3&gt;
&lt;p&gt;用阿里云推荐的 PG 18 部署，第一次启动 migration 就挂：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function uuidv7() is not unique
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dify migration 脚本里自定义了一个 &lt;code&gt;uuidv7()&lt;/code&gt;，而 PG 18 原生提供了同名函数，撞名后函数解析失败，迁移直接卡死。GitHub 上已经有相关 issue，PG 官方文档也明确写了 18 引入了 &lt;code&gt;uuidv7()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;最干脆的解法就是一开始就买 PG 15。RDS 不支持降版本，选错了只能重建实例。如果已经买了 18，手动改 migration 跳过冲突也能跑通，但每次 Dify 升级都要重新打补丁，不划算。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;LC_ALL&lt;/code&gt; warning&lt;/h3&gt;
&lt;p&gt;启动日志里经常看到这两行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
/entrypoint.sh: line 8: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不影响功能但看着烦。在 &lt;code&gt;.env&lt;/code&gt; 里加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LANG=C.UTF-8
LC_ALL=C.UTF-8
PYTHONIOENCODING=utf-8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dify 镜像里本来就是 &lt;code&gt;C.UTF-8&lt;/code&gt;，强制对齐就不会再报警告。&lt;/p&gt;
&lt;h2&gt;后续可以继续做的事&lt;/h2&gt;
&lt;p&gt;跑稳之后下一步可以朝几个方向继续优化，按需推进即可。&lt;/p&gt;
&lt;p&gt;水平扩展：文件全部走 OSS、session 走 Redis，应用层本来就具备横向扩展条件，加一台 ECS 前面套个 SLB 就能扩。&lt;/p&gt;
&lt;p&gt;高可用：RDS 和 Redis 都开主备实例，关键参数调成同步复制；Qdrant 升级为多副本集群部署；应用层做好健康检查 + 自动重启，最好接入 ECS 自愈。&lt;/p&gt;
&lt;p&gt;可观测性：日志用阿里云 SLS 或自建 Loki 统一收集；监控重点盯数据库连接数、Redis 内存、Qdrant 查询延迟、API 5xx 比例；链路追踪可以接 Jaeger 或阿里云 ARMS，Dify 已经原生支持 OpenTelemetry。&lt;/p&gt;
&lt;p&gt;备份与容灾：RDS 自动备份开起来（每天全量 + 增量，保留至少 7 天）；Qdrant 用 snapshot 接口定期备份到 OSS；OSS 配置跨区域复制。&lt;/p&gt;
&lt;h2&gt;一点小结&lt;/h2&gt;
&lt;p&gt;回头看整套部署，最值钱的判断其实就一句：&lt;strong&gt;有状态和无状态彻底分开。&lt;/strong&gt; 拆完之后所有复杂度都被分摊到了各自的领域里，数据库、应用、向量库可以各自演进、各自升配、各自备份，不会一个挂全挂。&lt;/p&gt;
&lt;p&gt;前期多花点时间把架构画清楚、把那几个版本和网络的坑踩完，换来的是后续比较长一段时间不用大动结构的可维护性。剩下的事就是把监控、备份、扩展这些按需补齐。希望这份记录能让正在走同样路的人少踩几个坑，把 Dify 真正用到业务里去。&lt;/p&gt;
</content:encoded><category>工程实践</category><author>Teclado</author></item><item><title>英语学习方法论原则篇</title><link>https://blog.teclado.cn/posts/yingyutu-guide-1/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/yingyutu-guide-1/</guid><pubDate>Tue, 30 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;记录一下学到的六大英语兔原则，它是学习英语的“心法”和“地图”，从根本上解决了“为什么学”和“学什么”的认知问题，是英语学习的道，将这六大原则作为英语学习的总纲全面贯彻到实战七大板块中。&lt;/p&gt;
&lt;h2&gt;1.重塑「英语」的根本定义&lt;/h2&gt;
&lt;p&gt;英语分为知识型英语和本能型英语，需要理解这两种英语学习的区别，知识型英语的学习好比学习历史，需要理解和记忆知识点；本能型英语学习好比学骑自行车，需要不断尝试习得技能。我们的大脑对于这两种模式的学习使用两套不同的记忆系统，学习知识型英语像学习历史知识点使用陈述性记忆用于存储事实理论概念，学习本能型英语好比骑自行车使用的是程序性记忆用于存储肌肉记忆本能反应。在以前的英语学习中如果把知识型英语的学习当做英语学习的全部，只背单词学语法就无法习得英语的本能反应，导致哑巴英语，正确的学习方式是用知识去指导练习，再通过练习将知识内化成本能。只有内化为本能才能在实际使用时不假思索的说出流利正确的英语。&lt;/p&gt;
&lt;p&gt;语⾔的本质，尤其是交流，恰恰更像骑⾃⾏⻋，⽽不是背历史。&lt;/p&gt;
&lt;p&gt;传统英语教育的最⼤问题，就是⽤“学历史”的⽅式去学英语：只训练陈述性记忆，让单词和语法停留在“知道”的层⾯，却从未训练程序性记忆，让语⾔能⼒达到“做到”的境界。&lt;/p&gt;
&lt;p&gt;哑巴英语的本质是你在试图⽤缓慢、有意识的“知识型”系统，去应对⼀个需要快速、⾃动化的“本能型”场景。真正正确的学习路径是：⽤“知识”去指导“练习”，再通过“练习”将“知识”内化为“本能”。&lt;/p&gt;
&lt;h2&gt;2.「英汉」底层逻辑的差异&lt;/h2&gt;
&lt;p&gt;英语和汉语是两套完全独⽴的系统，不仅是词汇的不同，更是在语⾳、语法、词汇搭配和思维逻辑上有着根本性的底层差异。所谓的“中式英语”，其本质原因是我们试图⽤⼀套根深蒂固的“中⽂操作系统”
去强⾏运⾏“英语应⽤程序”，导致了系统不兼容、卡顿和报错。&lt;/p&gt;
&lt;p&gt;学习英语的核⼼任务，不是简单地进⾏语⾔符号的“翻译”，⽽是要建⽴并切换到⼀套全新的“英语操作系统”，尊重其独特的发⾳规律、结构逻辑和表达习惯。&lt;/p&gt;
&lt;p&gt;⼀句英语的语法正确，不等于它是地道英语。地道的英语意味着你使⽤的是英语系统⾥的“原⽣”思维逻辑，⽽不是⽤汉语思维“原创”英语。学习英语就是学习“切换系统”。这条原则并⾮要求你真的“忘记⺟语”，⽽是要求你承认并尊重两种语⾔的“独⽴”属性。&lt;/p&gt;
&lt;h2&gt;3.遵循「i+1」原则&lt;/h2&gt;
&lt;p&gt;想要真正习得英语，必须确保输⼊满⾜“i+1”原则。“i”是你现在的⽔平，“1”是稍微⾼出⼀点点的难度。太难的内容（i+10）会让你进⼊“深海溺⽔区”，导致焦虑和放弃；太简单的内容（i-1）会让你留在“⼉童戏⽔池”，导致原地踏步。只有“i+1”才是⼤脑能吸收的有效养分。&lt;/p&gt;
&lt;p&gt;有效输⼊必须建⽴在“低情感过滤”的状态下。当你感到焦虑、紧张、或者被迫学习⽆聊内容时，⼤脑的防御机制会升起⼀道“墙”，挡住语⾔的进⼊。只有当你忘记⾃⼰在学习，完全沉浸在故事或内容本⾝带来的乐趣中时，语⾔习得的⼤⻔才会真正打开。&lt;/p&gt;
&lt;p&gt;质的积累必须伴随“⾜量”的⽀撑。语⾔习得是⼀个概率游戏，⼤脑需要海量的数据来归纳模式。如果没有⾜够的输⼊量作为“⽶”，⼤脑这个“巧妇”就⽆法通过归纳法⾃然习得语⾔，最终只能被迫退回到⽤中⽂逻辑去⽣造英语的“中式英语”死胡同。&lt;/p&gt;
&lt;p&gt;“习得”的⻩⾦时刻：⼤脑在最放松、最专注的状态下，畅通⽆阻地吸收了那些“i+1”的语料。&lt;/p&gt;
&lt;p&gt;语⾔“习得”的本质，是⼀个“量变引起质变”的概率游戏。你的⼤脑需要多次在不同情境下遇到同⼀个单词或句型，才能最终“内化”它，形成“本能”。&lt;/p&gt;
&lt;h2&gt;4.开启语言内化&lt;/h2&gt;
&lt;p&gt;输⼊（听读）决定了你语⾔能⼒的“上限”，即你的潜在知识库有多⼤；⽽输出（说写）决定了你语⾔能⼒的“下限”，即你能真正调动和使⽤的能⼒有多少。⼤多数⼈的痛苦在于上限很⾼（能听懂），但下限极低（说不出），中间巨⼤的落差就是“⽆法内化”的表现。&lt;/p&gt;
&lt;p&gt;刻意输出的三步闭环：真正的输出不是随意的闲聊或跟读，⽽是“主动的、有意识的加⼯”。它必须包含三个步骤：第⼀是主动使⽤，强迫⾃⼰跳出舒适区，使⽤刚学的新知识；第⼆是核查验证，卡壳时⽴刻查找标准答案，⽽不是糊弄过去；第三是重复强化，通过间隔重复将“知道”的知识刷成“会⽤”的本能。&lt;/p&gt;
&lt;p&gt;输出不是单⼀维度的，⽽是分层级的。从最低级的“照本宣科”（朗读、抄写），到中级的“语义重组”（复述、摘要），再到⾼级的“真实博弈”（社交对话），最后达到最⾼级的“重构教学”（教别⼈）。层级越⾼，⼤脑的加⼯深度越深，内化效果越彻底。&lt;/p&gt;
&lt;p&gt;输⼊决定了你能⼒发展的“上限”，你的知识库有多⼤，你的理解⼒就有多深；⽽输出决定了你能⼒实现的“下限”，你能说出多少，写出多少，才是你真正拥有的英语能⼒。&lt;/p&gt;
&lt;p&gt;随意的、不过脑⼦的输出，并不能带来真正的进步。⽽“刻意输出”，是⼀种主动的、有意识的“加⼯”⾏为。“教”之所以是最⾼级的“刻意输出”，因为它强迫你的⼤脑去进⾏最深度的“加⼯”。&lt;/p&gt;
&lt;h2&gt;5.植入「错误免疫」系统&lt;/h2&gt;
&lt;p&gt;那些被⻓期容忍的“⽼⽑病”，会共同构成了⼀层坚固且透明的“天花板”，将你的英语⽔平牢牢地锁死在“够⽤，但不够好”的尴尬境地。“流利”并不等于“正确”，也不代表英语能⼒的终点。如果只追求流利⽽忽视正确性，或者在没有反馈的情况下⻓期重复错误的表达，会导致语⾔能⼒的“⽯化”，让你被锁死在“够⽤但不够好”的中级平台期。&lt;/p&gt;
&lt;p&gt;语法规则和“知识型英语”在⼝语中的真正作⽤，是充当你的“监控器”。它的功能不是⽤来⽣成语⾔，⽽是在你本能输出语⾔后，充当编辑和校对的⻆⾊，帮助你修正错误，防⽌习惯性错误的固化。&lt;/p&gt;
&lt;p&gt;对抗⽯化的核⼼⼼法是“分离场景”。你需要建⽴双重⾝份：在“场上⽐赛”（⾃由交流）时，为了流利度要敢于犯错，关闭监控器；在“场下训练”（复盘练习）时，必须像侦探⼀样⽄⽄计较，激活监控器进⾏精准纠错。&lt;/p&gt;
&lt;p&gt;⼤多数学习者的问题在于，他们混淆了这两个场景：⽐如，在“⽐赛”（交流）时，他们过度担⼼犯错，像在“训练”时⼀样束⼿束脚；反过来，他们在“训练”（练习）时，⼜像在“⽐赛”⼀样只顾流利，忽视反馈和纠错，导致错误被反复练习，最终“⽯化”。&lt;/p&gt;
&lt;h2&gt;6. 拒绝「线性学习」思维&lt;/h2&gt;
&lt;p&gt;英语学习绝⾮线性的阶梯式上升，⽽是⽹状的螺旋式迭代。试图⼀次性彻底掌握某个知识点不仅违背⼤脑认知规律，更是导致挫败感和半途⽽废的根源。我们必须接受“遗忘”和“平台期”是学习过程中的必然常态。&lt;/p&gt;
&lt;p&gt;⾼效的学习路径类似于“多层绘画”⽽⾮“单次打印”。⽆论是语⾳、词汇还是语法，都需要通过多轮次的“重逢”来不断加深理解，从建⽴模糊轮廓到填充底⾊，再到精细刻画，最终实现从“知识”到“本能”的内化。&lt;/p&gt;
&lt;p&gt;遗忘是常态，⽽⾮失败：咱们的⼤脑天⽣就会遗忘。如果不进⾏复习，咱们很快就会忘记所学的⼤部分内容。所以，“学了就忘”不是你的问题，⽽是⼈类⼤脑的默认设置。&lt;/p&gt;
&lt;p&gt;你的⾸要⽬标不是“学好”，⽽是“坚持”。每天 15 分钟的“复利”远胜于周末 3⼩时的“⼀次性投⼊”。英语学习不是⼀场有终点的赛跑，⽽是⼀场终⾝的、不断带来新⻛景的探索之旅。当你不再为“学了就忘”⽽焦虑，不再为“原地踏步”⽽沮丧，⽽是真正理解并接受了这种“螺旋式”的进程时，你就掌握了语⾔学习的终极⼼法。&lt;/p&gt;
&lt;p&gt;维持螺旋上升的关键引擎是“低摩擦的习惯系统”⽽⾮不可靠的“意志⼒”。通过优化物理路径、拆解最⼩⾏动单元、锚定即时乐趣以及可视化复利进程，让坚持变得毫不费⼒，从⽽积累巨⼤的时间复利。&lt;/p&gt;
</content:encoded><category>英语</category><author>Teclado</author></item><item><title>Cloudreve 私有网盘搭建记录</title><link>https://blog.teclado.cn/posts/cloudreve-record/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/cloudreve-record/</guid><pubDate>Fri, 15 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当你有闲置的阿里云 ECS 和 OSS 服务时，可以尝试搭建基于 OSS 的内网中转的 Cloudreve 网盘，喜欢折腾的可以尝试一下，将 Cloudreve 部署在阿里云 ECS 上，然后配置 OSS 内网 Endpoint 使用内网流量将 OSS 上的文件先下载到 ECS 上，在通过 ECS 的固定带宽下载的自己的设备上。&lt;/p&gt;
&lt;p&gt;这套方案上传是直接本地上传到 OSS，流量免费速度很快，下载的速度很大程度取决于 ECS 的公网带宽，这里我 3M 的带宽下载速度稳定在 400KB/s，速度还可以，不搞大文件日常使用够了，主要是基于内网中转不用流出流量费，性价比高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./oss.svg&quot; alt=&quot;osstouser&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;具体步骤&lt;/h2&gt;
&lt;h3&gt;准备 docker 环境&lt;/h3&gt;
&lt;p&gt;阿里云 ECS 安装 Linux 发行版，我这里安装的是 Alibaba Cloud Linux 3，熟悉命令行的话命令行安装好 docker 和 docker compose 环境，不熟悉命令行其实安装宝塔面板更方便，新机器可以直接在镜像市场选择官方的操作系统➕宝塔面板镜像。&lt;/p&gt;
&lt;p&gt;我这里使用面板安装 docker，操作非常简单，登陆宝塔面板，点击左侧导航栏下方左边的&amp;lt;菜单显示隐藏设置&amp;gt;，docker 默认是隐藏的，点击打开，然后在菜单栏进入 docker 页面，点击安装 docker，等待安装完成 docker 和 docker compose 就安装上了，是不是非常容易。&lt;/p&gt;
&lt;h3&gt;安装 Cloudreve&lt;/h3&gt;
&lt;p&gt;在面板找到文件菜单栏，在合适的位置新建一个目录，把 Cloudreve 社区版 &lt;a href=&quot;https://github.com/cloudreve/Cloudreve/blob/master/docker-compose.yml&quot;&gt;docker-compose.yml&lt;/a&gt; 文件上传上去，打开命令行运行 docker compose up -d，等待镜像拉取然后等容器成功起来后，去到 ECS 安全组放行 5212 入端口，然后浏览器输入你的公网 IP+5212 端口就能访问 Cloudreve 了。&lt;/p&gt;
&lt;h3&gt;配置存储策略&lt;/h3&gt;
&lt;p&gt;如果 ECS 有空闲的数据盘可以配一个本地存储策略，也就是数据直接存在 ECS 上不用上传到 OSS，但是社区版 Cloudreve 用户组只能配一种存储策略还是挺难受的，一般只能建两个用户组，一个使用本地存储，另一个使用 OSS 内网中转。&lt;/p&gt;
&lt;p&gt;使用阿里云 OSS 的好处是和 ECS 同地域内网访问只需要付存储费用和少量请求费用，大头流量费用可以省也就是这个方案有意义的地方，OSS 存储空间一般有活动买资源包和预留空间有优惠，用不完就可以用来搭个网盘高效利用起来。&lt;/p&gt;
&lt;p&gt;策略配置这块还挺简单的，官方文档很清晰，照着做就没问题，注意点就是配置阿里云 OSS 存储策略时先使用公网 Endpoint 创建策略，然后在编辑策略中添加内网 Endpoint 并且打开中转下载，这样上传走直连下载走中转，不用流量费用。&lt;/p&gt;
&lt;h3&gt;配置用户组&lt;/h3&gt;
&lt;p&gt;新建一个用户组把阿里云 OSS 内网中转存储策略分配给它，然后创建日常使用账号归属这个用户组，关闭网站注册功能，登陆账号测试上传和下载。&lt;/p&gt;
&lt;h3&gt;分配域名&lt;/h3&gt;
&lt;p&gt;到目前为止只能通过 IP 地址加端口的方式访问，这样不安全最好是分配一个二级域名给他 Cloudreve，这样配上 SSL 证书就能通过 https 访问。&lt;/p&gt;
&lt;p&gt;这里我的做法是在阿里云解析 DNS 中在我的个人域名中加上一条解析记录，将这个 cloudreve.teclado.cn 二级域名 A 类型解析到服务器 IP 地址上，然后在宝塔面板 &amp;lt;网站&amp;gt; 菜单栏里选择反向代理然后新建一个反向代理项目，将域名 cloudreve.teclado.cn 反向代理到公网 IP:5212 地址就可以。&lt;/p&gt;
&lt;p&gt;如果有泛域名证书就直接配置一下，没有也可以很方便的申请一个单域名 CA 证书部署上去。&lt;/p&gt;
&lt;h2&gt;使用感受&lt;/h2&gt;
&lt;p&gt;受限于资源自己搭建的网盘在空间和速度上其实都远远比不上商业网盘产品，自己搭建的网盘存储空间在 10GB 到 20GB，速度约 400KB/s，空间上比不上百度网盘免费送的 1T 空间，速度上 400KB/s 倒比很多网盘快一些，免费网盘的下载速度都在 100KB/s 左右，百度这种可能只有 30KB/s，所以说自己搭建的网盘在空间和速度上还是足够用的，基本工作场景是没问题的，但是如果真有各种备份同步需求，还是花点钱开个网盘年费会员最省事划算，不要折腾自建这个方案了。&lt;/p&gt;
&lt;p&gt;所以总结自建网盘最大的优势是数据自控不会随意被和谐，以及资源利用（当你有空闲的 OSS 空间时就可以用来当自己的网盘空间，前提是你有阿里云 ECS），此外对于低需求的用户而言，Cloudreve 网盘提供了链接分享和离线下载等有用的功能，把它当做专门的资源分享器也不错，而且离线下载功能大多数网盘都是会员功能，Cloudreve 免费提供，当做离线下载器用也是好的。&lt;/p&gt;
&lt;p&gt;最后，&lt;a href=&quot;https://docs.cloudreve.org/zh/&quot;&gt;Cloudreve 官方文档&lt;/a&gt;写的很清晰，有动手能力的朋友可以尝试一下，不算太折腾，挺好玩的。&lt;/p&gt;
</content:encoded><category>工程实践</category><author>Teclado</author></item><item><title>uv 入门使用指南</title><link>https://blog.teclado.cn/posts/uv-guide/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/uv-guide/</guid><pubDate>Wed, 23 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Python 生态系统中的包管理一直是开发者关注的焦点。从最初的 pip 到后来的 Poetry、PDM 等工具，每一次演进都试图解决前代工具的痛点。而今天我们要介绍的 UV，作为由 Ruff 团队（Astral）开发的下一代 Python 包管理工具，正以其惊人的速度和创新的设计理念，重新定义 Python 包管理的标准。&lt;/p&gt;
&lt;h2&gt;UV 介绍&lt;/h2&gt;
&lt;h3&gt;什么是 UV&lt;/h3&gt;
&lt;p&gt;UV 是一个极速的 Python 包管理器和解析器，由 Rust 编写，专注于提供卓越的性能和用户体验。其名称 &quot;UV&quot; 源自 &quot;μv&quot;（微伏），暗示着其轻量级和高效的特性。作为 pip 的直接替代品，UV 提供了兼容的命令行接口，同时在速度上实现了数量级的提升。&lt;/p&gt;
&lt;h3&gt;UV 的核心优势&lt;/h3&gt;
&lt;p&gt;与传统的 Python 包管理工具相比，UV 具有以下显著优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;🚀 一体化工具&lt;/strong&gt;：一个工具替代 pip、pip-tools、pipx、poetry、pyenv、twine、virtualenv 等多种工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;⚡️ 极致速度&lt;/strong&gt;：比 pip 快 10-100 倍&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🗂️ 全面项目管理&lt;/strong&gt;：提供通用锁文件的综合项目管理功能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;❇️ 脚本运行&lt;/strong&gt;：支持带有内联依赖元数据的脚本运行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🐍 Python 版本管理&lt;/strong&gt;：安装和管理不同的 Python 版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🛠️ 工具安装&lt;/strong&gt;：运行和安装以 Python 包形式发布的工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🔩 兼容pip接口&lt;/strong&gt;：提供熟悉的命令行界面，同时大幅提升性能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🏢 工作区支持&lt;/strong&gt;：支持 Cargo 风格的工作区，适用于大规模项目&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;💾 高效磁盘空间利用&lt;/strong&gt;：通过全局缓存实现依赖去重&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;⏬ 简易安装&lt;/strong&gt;：无需 Rust 或 Python 环境，可通过 curl 或 pip 直接安装&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🖥️ 多平台支持&lt;/strong&gt;：支持 macOS、Linux 和 Windows 系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随着 Python 项目规模和复杂度的增长，传统工具在性能和用户体验方面的局限性日益凸显。UV 的出现，为开发者提供了一个更快、更可靠的选择，特别适合大型项目和 CI/CD 环境中的应用。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;UV 的安装非常简单，支持多种操作系统和安装方式。以下是几种常见的安装方法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用官方安装脚本（推荐）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Linux/macOS
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows
powershell -ExecutionPolicy ByPass -c &quot;irm https://astral.sh/uv/install.ps1 | iex&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用包管理器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# macOS (Homebrew)
brew install uv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，可以通过以下命令验证安装是否成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装正确会得到版本号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv 0.8.0 (0b2357294 2025-07-17)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;python 版本管理&lt;/h2&gt;
&lt;p&gt;如果系统中已安装 Python，uv 会&lt;strong&gt;自动检测并使用&lt;/strong&gt;，无需额外配置。不过，uv 也能够安装和管理 Python 版本，uv 会根据需要自动安装缺失的 Python 版本，因此你无需预先安装 Python 即可上手。&lt;/p&gt;
&lt;p&gt;安装最新版本的 Python：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv python install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 官方并未发布可分发的二进制文件，uv 使用的是 Astral 的 &lt;a href=&quot;https://github.com/astral-sh/python-build-standalone&quot;&gt;python-build-standalone&lt;/a&gt; 项目提供的发行版。uv 安装的 Python 不会全局可用（即无法通过 python 命令调用）需要使用 uv run 或创建并激活虚拟环境来使用下载的 Python。&lt;/p&gt;
&lt;p&gt;Python 版本由 Python 解释器（即 python 可执行文件）、标准库和其他支持文件组成，由于系统中通常已安装 Python，uv 支持&lt;strong&gt;发现&lt;/strong&gt; Python 版本。不过，uv 也支持 &lt;strong&gt;自行安装&lt;/strong&gt; Python 版本。为区分这两种 Python 安装类型，uv 将其安装的 Python 版本称为&lt;em&gt;托管&lt;/em&gt; Python 安装，而将所有其他 Python 安装称为&lt;em&gt;系统&lt;/em&gt; Python 安装。&lt;/p&gt;
&lt;p&gt;uv 不会区分操作系统安装的 Python 版本与其他工具安装和管理的 Python 版本。例如，如果使用 pyenv 管理 Python 安装，在 uv 中它仍会被视为&lt;em&gt;系统&lt;/em&gt; Python 版本。&lt;/p&gt;
&lt;p&gt;安装特定版本的 Python：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv python install 3.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装多个 Python 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv python install 3.11 3.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装其他 Python 实现，例如 PyPy&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv python install pypy@3.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要重新安装由 uv 管理的 Python 版本，使用参数 reinstall，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv python install 3.12 --reinstall
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要查看可用和已安装的 Python 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv python list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;卸载安装的 Python 版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv python uninstall 3.12
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;脚本运行&lt;/h2&gt;
&lt;p&gt;这里的脚本指的是独立执行的 python 文件，例如使用 python example.py 来执行 example.py 这个脚本文件，使用 uv 执行脚本可以在无需手动管理环境的情况下管理脚本的依赖项。也就是说传统的使用 python 解释器运行脚本文件的方式下，当脚本需要第三方软件包依赖时，需要在全局环境中安装依赖，意味脚本依赖管理混乱，使用 uv 可以在不用手动管理环境的情况下为每个脚本管理依赖和创建临时虚拟环境。&lt;/p&gt;
&lt;p&gt;当运行无依赖或者只依赖标准库中的某些模块的脚本时，直接使用 uv run 来执行脚本。需要注意的是，pyproject.toml 文件用来确定项目根目录，当 uv run 时如果目录中存在 pyproject.toml 文件 uv 会安装文件中的依赖，当脚本在项目中但是不依赖项目时，使用 --no-project 选项来跳过检查项目文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 注意：`--no-project` 标志必须在脚本名称之前提供。
uv run --no-project example.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你的脚本需要其他软件包时，必须将它们安装到脚本运行的环境中，uv 倾向于按需创建这些环境，而不是使用手动管理依赖项的长期虚拟环境。按需创建环境意味着当脚本执行时由 uv 来创建所需的环境，区别于项目的手动管理环境。对于脚本所需的依赖项，通常使用&lt;strong&gt;项目&lt;/strong&gt;或&lt;strong&gt;内联元数据&lt;/strong&gt;来声明依赖项，但 uv 也支持每次调用时请求依赖项。&lt;/p&gt;
&lt;p&gt;例如，以下脚本需要 rich 这个包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# example.py
import time
from rich.progress import track

for i in track(range(20), description=&quot;For example:&quot;):
    time.sleep(0.05)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在未指定依赖项的情况下执行，此脚本将失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run --no-project example.py	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 --with 选项请求依赖项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run --with rich example.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 最近为&lt;a href=&quot;https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata&quot;&gt;内联脚本元数据&lt;/a&gt;添加了一种标准格式，它允许选择 Python 版本并定义依赖项。使用 uv init --script 来初始化带有内联元数据的脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv init --script example.py --python 3.13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内联元数据格式允许在脚本本身中声明脚本的依赖项，uv 支持添加和更新内联脚本元数据，使用 uv add --script 来声明脚本的依赖项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add --script example.py &apos;requests&amp;lt;3&apos; &apos;rich&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将在脚本顶部添加一个 script 部分，使用 TOML 声明依赖项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /// script
# dependencies = [
#   &quot;requests&amp;lt;3&quot;,
#   &quot;rich&quot;,
# ]
# ///

import requests
from rich.pretty import pprint

resp = requests.get(&quot;https://peps.python.org/api/peps.json&quot;)
data = resp.json()
pprint([(k, v[&quot;title&quot;]) for k, v in data.items()][:10])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用内联脚本元数据时，即使 uv run 在项目中使用，项目的依赖项也将被忽略，无需使用 --no-project 选项，uv 将自动创建一个包含运行脚本所需依赖项的环境来执行脚本。&lt;/p&gt;
&lt;h2&gt;使用工具&lt;/h2&gt;
&lt;h3&gt;运行工具&lt;/h3&gt;
&lt;p&gt;这里的工具指的是提供&lt;strong&gt;命令行交互&lt;/strong&gt;的软件包，uv 包含一个用于与工具交互的专用接口，可以使用 uv tool run 无需安装即可调用工具，在这种情况下，其依赖项将安装在与当前项目隔离的临时虚拟环境中。官方提供 uvx 作为 uv tool run 命令的别名，这两个命令完全等效，官方推荐使用 uvx。&lt;/p&gt;
&lt;p&gt;uvx 命令可在不安装工具的情况下调用它，可在工具名称后提供参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx ruff
# 等效于
uv tool run ruff
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;uvx pycowsay hello from uv
  -------------
&amp;lt; hello from uv &amp;gt;
  -------------
   \   ^__^
    \  (oo)\_______
       (__)\       )\/\
           ||----w |
           ||     ||
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当调用 uvx ruff 时，uv 会安装提供 ruff 命令的 ruff 软件包。然而，有时软件包名和命令名会有所不同，可使用 --from 选项从特定软件包调用命令，例如由 httpie 提供的 http 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx --from httpie http
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;--from 还可以指定请求特定版本和不同源的软件包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx --from &apos;ruff==0.3.0&apos; ruff check
uvx --from git+https://github.com/httpie/cli httpie
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装工具&lt;/h3&gt;
&lt;p&gt;如果经常使用某个工具，就可以将其安装到持久化环境并添加到环境 PATH 中，这样就无需反复调用 uvx 运行工具了。与工具相关的命令都以 uv tool 开始，因为 run 命令常用，所以把 uvx 作为 uv tool run 的别名使用。&lt;/p&gt;
&lt;p&gt;安装 ruff 工具：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv tool install ruff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装工具后，其可执行文件会被放置在 PATH 中的 bin 目录下，这样就可以不通过 uv 来运行该工具。如果它不在 PATH 中，会显示一条警告，此时可以使用 uv tool update-shell 将其添加到 PATH 中。&lt;/p&gt;
&lt;p&gt;安装 ruff 后，它应该就可以使用了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ruff --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 uv tool install 安装工具时，将在 uv 工具目录中创建一个虚拟环境，除非卸载该工具，否则该环境不会被删除，如果手动删除该环境，工具将无法运行，查看工具目录位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv tool dir
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意，安装工具并不能使其模块在当前环境中可用，uv 隔离管理了工具、脚本和项目环境，来减少相互影响和冲突。&lt;/p&gt;
&lt;p&gt;与 uvx 不同，uv tool install 操作的是一个 &lt;em&gt;包&lt;/em&gt;，并且会安装该工具提供的所有可执行文件，所以无需 --from 即可指定包版本和来源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv tool install &apos;httpie&amp;gt;0.1.0&apos;
uv tool install git+https://github.com/httpie/cli
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;项目管理&lt;/h2&gt;
&lt;h3&gt;创建新项目&lt;/h3&gt;
&lt;p&gt;你可以使用 uv init 命令创建一个新的 Python 项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv init hello-world
cd hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，你也可以在当前工作目录中初始化一个项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir hello-world
cd hello-world
uv init
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;项目结构&lt;/h3&gt;
&lt;p&gt;一个项目由几个重要部分组成，它们协同工作，使 uv 能够管理你的项目，除了 uv init 创建的文件外，在你首次运行项目命令（如 uv run、uv sync 或 uv lock）时，uv 还会在项目根目录中创建一个虚拟环境和 uv.lock 文件。&lt;/p&gt;
&lt;p&gt;完整的文件列表如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── .venv
│   ├── bin
│   ├── lib
│   └── pyvenv.cfg
├── .python-version
├── README.md
├── main.py
├── pyproject.toml
└── uv.lock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pyproject.toml 包含项目的元数据，这个文件用来记录依赖项，以及项目的详细信息，如项目描述或许可证，你可以手动编辑此文件，一般来说是使用 uv add 和 uv remove 等命令从终端管理项目。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project]
name = &quot;hello-world&quot;
version = &quot;0.1.0&quot;
description = &quot;在此处添加项目描述&quot;
readme = &quot;README.md&quot;
dependencies = []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;.python-version 文件包含项目的默认 Python 版本，此文件告诉 uv 在创建项目的虚拟环境时应使用哪个 Python 版本。&lt;/p&gt;
&lt;p&gt;.venv 文件夹包含项目的虚拟环境，这是一个与系统其他部分隔离的 Python 环境，uv 将在此处安装项目的依赖项。&lt;/p&gt;
&lt;p&gt;uv.lock 是一个跨平台的锁定文件，其中包含有关项目依赖项的确切信息，与用于指定项目大致要求的 pyproject.toml 不同，锁定文件包含安装在项目环境中的确切解析版本，此文件应提交到版本控制系统，以便在不同机器上实现一致且可重现的安装，uv.lock 是一个人类可读的 TOML 文件，但由 uv 管理，不应手动编辑。&lt;/p&gt;
&lt;h3&gt;管理依赖&lt;/h3&gt;
&lt;p&gt;你可以使用 uv add 命令将依赖项添加到 pyproject.toml 中，这也会更新锁定文件和项目环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要移除一个包，可以使用 uv remove：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv remove requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要升级一个包，可以使用带 --upgrade-package 标志的 uv lock 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv lock --upgrade-package requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;--upgrade-package 选项会尝试将指定的包更新到最新的兼容版本，同时保持锁定文件的其余部分不变。&lt;/p&gt;
&lt;h3&gt;运行命令&lt;/h3&gt;
&lt;p&gt;uv run 可用于在项目环境中运行任意脚本或命令，在每次调用 uv run 之前，uv 会验证锁定文件是否与 pyproject.toml 保持同步，并且环境是否与锁定文件保持同步，从而无需手动干预即可使项目保持最新状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add flask
uv run -- flask run -p 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意这里 -- 是 Unix/Linux 命令行通用约定，cmd1 -- cmd2 args 显式声明 cmd2 args 作为完整命令执行，在这里 uv run 激活虚拟环境，然后 flask run -p 3000 单独执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者，你可以使用 uv sync 手动更新环境，然后在执行命令前激活环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv sync
source .venv/bin/activate
flask run -p 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构建项目&lt;/h3&gt;
&lt;p&gt;uv build 可用于为你的项目构建源发行版和二进制发行版（wheel），默认情况下，uv build 将在当前目录中构建项目，并将构建产物放置在 dist 子目录中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv build
ls dist/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;发布包&lt;/h3&gt;
&lt;p&gt;uv 支持通过 uv build 将 Python 包构建为源码和二进制发行版，并通过 uv publish 将它们上传到注册中心。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv publish
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;文档看到现在基本了解的 uv 的用法和原理，因为大多数编程语言都有自己的包管理器，所以理解起来还不算太难，更多操作细节需要在实践中去摸索。&lt;/p&gt;
</content:encoded><category>编程</category><author>Teclado</author></item><item><title>venv 使用和原理</title><link>https://blog.teclado.cn/posts/python-venv/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/python-venv/</guid><pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在学习 uv 的过程中对 uv venv 虚拟环境的使用一直有点困惑，研究后发现 uv venv 底层是基于 python 内置的 venv 模块实现，但是我对 venv 也不了解，而且之前一直把虚拟环境和 Conda 的独立环境混在一起所以这篇是记录一下 python venv 模块原理和使用。&lt;/p&gt;
&lt;h2&gt;为什么需要虚拟环境&lt;/h2&gt;
&lt;p&gt;虚拟环境要做的事是&lt;strong&gt;隔离项目依赖&lt;/strong&gt;，想象一下这样一个场景，你有两个 python 项目使用到了 Flask 2.0.1，如果其中一个项目需要升级 Flask 版本到 3.0，那么问题来了，如果你是在系统全局 python 环境中安装的 Flask，当你把 Flask 从 2.0.1 升级到 3.0 后，就会破坏另一个项目的 Flask 依赖（因为它被锁定使用 Flask 2.0.1），如果另外再安装一个 Flask 2.0.1 来供原来的项目使用，就会让所有项目的包都混在一起管理。&lt;/p&gt;
&lt;p&gt;Python 3.3+ 内置了 venv 模块（替代早期的 virtualenv），来为每个项目创建隔离的 Python 运行环境。&lt;/p&gt;
&lt;h2&gt;创建和使用虚拟环境&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;创建环境&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建名为 .venv 的虚拟环境（推荐使用 .venv 作为默认名）
python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;激活环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Windows
.venv\Scripts\activate

# Linux/macOS
source .venv/bin/activate

# 激活后提示符变化示例：
(.venv) user@machine:~$
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装包（隔离在虚拟环境中）
(.venv) pip install django==4.2

# 运行代码
(.venv) python my_app.py

# 导出依赖
(.venv) pip freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;退出环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;deactivate
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;底层原理&lt;/h2&gt;
&lt;p&gt;当你使用 python -m venv .venv 创建一个的虚拟环境后，你会看到类似这样的目录结构（在 Unix/macOS 为 bin，Windows 中 Scripts 代替 bin）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.venv/
├── bin/               # Unix/macOS 执行文件
│   ├── python         -&amp;gt; /usr/bin/python3.10 (符号链接)
│   ├── pip            # 专属 pip 脚本
│   └── activate       # 环境激活脚本
├── Scripts/           # Windows 执行文件
│   ├── python.exe     # 解释器链接
│   ├── pip.exe
│   └── activate.bat
├── lib/
│   └── python3.10/
│       └── site-packages/  # 核心！所有安装的包存放于此
└── pyvenv.cfg          # 配置文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最重要的部分是 site-packages 目录，当激活虚拟环境后，后续所有安装的包都安装到这个目录下，系统全局的 site-packages 目录将不再访问。&lt;/p&gt;
&lt;p&gt;venv 的本质是将路径重定向到指定的环境目录，在激活环境时主要做了两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;修改 PATH 环境变量：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将虚拟环境目录下的 bin (或 Scripts) 目录添加到你的系统 PATH 变量的&lt;strong&gt;最前面&lt;/strong&gt;，这样，当你直接在命令行输入 python 或 pip 时，系统会优先找到并使用虚拟环境里的版本 ，而不是系统全局的那个。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置 VIRTUAL_ENV 环境变量：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个变量指向你的虚拟环境目录，很多工具会检查这个变量，知道当前在哪个虚拟环境中工作。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;.venv/bin/python 通常只是一个指向你创建环境时指定的那个 Python 解释器（比如 /usr/bin/python3.10）的&lt;strong&gt;符号链接&lt;/strong&gt;（或 Windows 上的快捷方式/小副本），它&lt;strong&gt;没有&lt;/strong&gt;在虚拟环境内部安装一个完全独立的 Python 副本（不像 Conda）。&lt;/p&gt;
</content:encoded><category>编程</category><author>Teclado</author></item><item><title>Chroma 向量数据库</title><link>https://blog.teclado.cn/posts/chrome-guide/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/chrome-guide/</guid><pubDate>Thu, 10 Jul 2025 13:50:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本篇文章内容来源于&lt;a href=&quot;https://docs.trychroma.com/docs/overview/introduction&quot;&gt;Chroma官方文档&lt;/a&gt;，是学习过程中的翻译整理，仅作参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Chroma 是开源 AI 应用数据库，支持多种部署方式（后面会介绍），当使用客户端-服务端模式部署时，官方提供了 python 和 JavaScript/Typescript 两种客户端 sdk，并且支持在 Jupyter Notebook 中运行。&lt;/p&gt;
&lt;h2&gt;快速入门&lt;/h2&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install chromadb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建客户端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import chromadb
chroma_client = chromadb.Client()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建集合&lt;/h3&gt;
&lt;p&gt;集合用于存储嵌入、文档以及其他元数据，集合会为你的嵌入和文档建立索引，并支持高效的检索和筛选。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection = chroma_client.create_collection(name=&quot;my_collection&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加文本数据&lt;/h3&gt;
&lt;p&gt;Chroma 将存储你的文本并&lt;strong&gt;自动处理嵌入和索引&lt;/strong&gt;，你还可以自定义嵌入函数，这里使用默认嵌入函数，你必须为文本提供唯一的字符串 id。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.add(
    ids=[&quot;id1&quot;, &quot;id2&quot;],
    documents=[
        &quot;This is a document about pineapple&quot;,
        &quot;This is a document about oranges&quot;
    ]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查询集合&lt;/h3&gt;
&lt;p&gt;你可以使用查询文本列表来查询集合，Chroma 将返回 n 个最相似的结果，n 默认是 10，这里设置为 2。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;results = collection.query(
    query_texts=[&quot;This is a query document about hawaii&quot;], # Chroma will embed this for you
    n_results=2 # how many results to return
)
print(results)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;检查结果&lt;/h3&gt;
&lt;p&gt;从输出可以看到，我们关于夏威夷的查询在语义上与关于菠萝的文档最相似。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &apos;documents&apos;: [[
      &apos;This is a document about pineapple&apos;,
      &apos;This is a document about oranges&apos;
  ]],
  &apos;ids&apos;: [[&apos;id1&apos;, &apos;id2&apos;]],
  &apos;distances&apos;: [[1.0404009819030762, 1.243080496788025]],
  &apos;uris&apos;: None,
  &apos;data&apos;: None,
  &apos;metadatas&apos;: [[None, None]],
  &apos;embeddings&apos;: None,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;亲自试一下&lt;/h3&gt;
&lt;p&gt;如果我们试着用 &quot;This is a query document about florida&quot; 来查询呢？下面是一个完整的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import chromadb
chroma_client = chromadb.Client()

# switch `create_collection` to `get_or_create_collection` to avoid creating a new collection every time
collection = chroma_client.get_or_create_collection(name=&quot;my_collection&quot;)

# switch `add` to `upsert` to avoid adding the same documents every time
collection.upsert(
    documents=[
        &quot;This is a document about pineapple&quot;,
        &quot;This is a document about oranges&quot;
    ],
    ids=[&quot;id1&quot;, &quot;id2&quot;]
)

results = collection.query(
    query_texts=[&quot;This is a query document about florida&quot;], # Chroma will embed this for you
    n_results=2 # how many results to return
)

print(results)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果参考：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ 
 &apos;documents&apos;: [[
     &apos;This is a document about oranges&apos;, 
     &apos;This is a document about pineapple&apos;
 ]], 
 &apos;ids&apos;: [[&apos;id2&apos;, &apos;id1&apos;]], 
 &apos;distances&apos;: [[1.1462137699127197, 1.3015384674072266]]
 &apos;included&apos;: [&apos;metadatas&apos;, &apos;documents&apos;, &apos;distances&apos;], 
 &apos;uris&apos;: None, 
 &apos;data&apos;: None, 
 &apos;metadatas&apos;: [[None, None]], 
 &apos;embeddings&apos;: None, 
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;架构设计&lt;/h2&gt;
&lt;p&gt;Chroma 采用&lt;strong&gt;模块化架构设计&lt;/strong&gt;，优先考虑性能和易用性，它可以无缝地从本地开发扩展到大规模生产，同时在所有部署模式中提供一致的 API。&lt;/p&gt;
&lt;p&gt;Chroma 尽可能地将数据持久性问题委托给 SQLite、Cloud Object Storage 等可信子系统，将系统设计重点放在数据管理和信息检索等核心问题上。&lt;/p&gt;
&lt;h3&gt;部署模式&lt;/h3&gt;
&lt;p&gt;Chroma 可以运行在任何你需要它的地方，支持从本地实验到大规模的生产工作负载。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local&lt;/strong&gt;：作为嵌入式库，非常适合原型和实验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single Node&lt;/strong&gt;：作为单节点服务器，非常适合小规模集合（小于 10M 记录）的工作负载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed&lt;/strong&gt;：作为一个可扩展的分布式系统，非常适合大规模生产工作负载，支持数百万个集合，你可以使用 &lt;a href=&quot;https://www.trychroma.com/signup&quot;&gt;Chroma Cloud&lt;/a&gt;，它是分布式 Chroma 的托管产品。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;核心组件&lt;/h3&gt;
&lt;p&gt;无论采用何种部署模式，Chroma 都由五个核心组件组成，每个在系统中扮演不同的角色，并在共享的 Chroma 数据模型上操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./architecture.png&quot; alt=&quot;architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gateways&lt;/strong&gt;：所有客户端通信的入口点，在所有模式中公开一致的API，处理身份验证、速率限制、配额管理和请求验证，将请求路由到下游服务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Distributed Log&lt;/strong&gt;：Chroma 的预写（write-ahead）日志，在向客户确认之前，所有的写都记录在这里，确保跨多记录写入的原子性，在分布式部署中提供持久性和重播性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Query Nodes&lt;/strong&gt;：负责所有的读操作，向量相似度、全文和元数据搜索，维护内存和磁盘索引的组合，并与日志协调以提供一致的结果，&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compactor Nodes&lt;/strong&gt;：定期构建和维护索引的服务，从日志中读取并构建更新的矢量/全文/元数据索引，将索引数据写入共享存储，使用有关新索引版本的元数据更新系统数据库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SysDb&lt;/strong&gt;：Chroma 的内部目录，跟踪租户、集合及其元数据，在分布式模式下，还管理集群状态，由 SQL 数据库支持。&lt;/p&gt;
&lt;h3&gt;存储和运行时&lt;/h3&gt;
&lt;p&gt;这些组件的操作方式取决于部署模式，特别是它们如何使用存储和运行时，在本地和单节点模式下，所有组件共享一个进程并使用本地文件系统以实现持久性。在分布式模式下，组件作为独立的服务部署，日志和构建的索引存储在云对象存储中，系统目录由SQL数据库提供支持，所有业务均使用本地 SSD 作为缓存，降低对象存储延迟和成本。&lt;/p&gt;
&lt;h3&gt;请求流程&lt;/h3&gt;
&lt;h4&gt;读请求路径&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./read_path.png&quot; alt=&quot;read_path&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;请求到达网关，在那里对其进行身份验证、检查配额限制、速率限制并转换为逻辑计划。&lt;/li&gt;
&lt;li&gt;此逻辑计划被路由到相关的查询执行程序。在分布式 Chroma 中，集合上的 id 哈希用于将查询路由到正确的节点，并提供缓存一致性。&lt;/li&gt;
&lt;li&gt;查询执行程序将逻辑计划转换为物理计划以供执行，从其存储层读取数据，并执行查询，查询执行程序从日志中提取数据以确保读取的一致性。&lt;/li&gt;
&lt;li&gt;请求被返回到网关，随后返回到客户端。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;写请求路径&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./write_path.png&quot; alt=&quot;write_path&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;请求到达网关，在那里对其进行身份验证、检查配额限制、速率限制，然后将其转换为操作日志。&lt;/li&gt;
&lt;li&gt;操作日志被转发到预写日志以实现持久化。&lt;/li&gt;
&lt;li&gt;在被预写日志持久化之后，网关返回确认写入。&lt;/li&gt;
&lt;li&gt;压缩器定期从预写日志中提取，并根据积累的写入操作构建新的索引版本，这些索引针对读取性能进行了优化，包括矢量、全文和元数据索引。&lt;/li&gt;
&lt;li&gt;一旦构建了新的索引版本，它们将被写入存储并在系统数据库中注册。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;权衡&lt;/h3&gt;
&lt;p&gt;分布式 Chroma 是建立在对象存储之上，旨在确保数据的持久性并降低成本，对象存储具有极高的吞吐量，能够轻松满足单个节点的网络带宽需求，但代价是延迟相对较高，约为 10-20 毫秒。&lt;/p&gt;
&lt;p&gt;为了减少这个延迟层的开销，分布式 Chroma 积极地利用 SSD 缓存。当您第一次查询一个集合时，回答查询所需的数据子集将有选择地从对象存储中读取，从而导致冷启动延迟，后台将收集的数据加载到 SSD 缓存，在收集完全热起来之后，查询将完全由 SSD 提供。&lt;/p&gt;
&lt;h2&gt;数据模型&lt;/h2&gt;
&lt;p&gt;Chroma 的数据模型旨在平衡简单性、灵活性和可扩展性。它引入了一些核心抽象概念：&lt;strong&gt;Tenants&lt;/strong&gt;（租户）、&lt;strong&gt;Databases&lt;/strong&gt;（数据库）和 &lt;strong&gt;Collections&lt;/strong&gt;（集合），让你能够跨环境和用例高效地组织、检索和管理数据。&lt;/p&gt;
&lt;h3&gt;Collections&lt;/h3&gt;
&lt;p&gt;集合是 Chroma 中存储和查询的基本单位。每个集合包含一组记录，其中每个记录包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唯一标识记录的 ID&lt;/li&gt;
&lt;li&gt;嵌入向量&lt;/li&gt;
&lt;li&gt;可选元数据（键值对）&lt;/li&gt;
&lt;li&gt;所提供嵌入的文档&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;集合是独立索引的，并使用向量相似性、全文搜索和元数据过滤进行了快速检索优化。在分布式部署中，可以根据需要对集合进行分片或跨节点迁移，系统根据访问模式透明地管理它们在内存中的分页。&lt;/p&gt;
&lt;h3&gt;Databases&lt;/h3&gt;
&lt;p&gt;集合被分组到数据库中，数据库充当逻辑命名空间，这对于按用途组织集合很有用，例如，将“暂存”和“生产”这样的环境区分开，或者按应用程序分到不同的数据库下。&lt;/p&gt;
&lt;p&gt;每个数据库包含多个集合，并且每个集合名称在数据库中必须是唯一的。&lt;/p&gt;
&lt;h3&gt;Tenants&lt;/h3&gt;
&lt;p&gt;Chroma 数据模型的顶层是租户，它代表单个用户、团队或帐户，租户之间完全隔离，租户之间不共享任何数据或元数据，所有访问控制、配额执行和计费均在租户级别进行。&lt;/p&gt;
&lt;h2&gt;运行 Chroma&lt;/h2&gt;
&lt;h3&gt;临时客户端&lt;/h3&gt;
&lt;p&gt;EphemeralClient() 方法在内存中启动一个 Chroma 服务器，并返回一个客户端，可以通过这个客户端连接到 Chroma 服务器。适合一些不需要数据持久化的场景，可以用来在 Jupyter notebook 中试验不同的嵌入函数和检索技术。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import chromadb

client = chromadb.EphemeralClient()
# client = chromadb.Client() 也是内存模式
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;持久客户端&lt;/h3&gt;
&lt;p&gt;PersistentClient() 方法将启动一个持久客户端，path 是 Chroma 在磁盘上存储数据库文件的地方，并在启动时加载它们，如果不提供路径，则默认为 .chroma。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import chromadb

client = chromadb.PersistentClient(path=&quot;/path/to/save/to&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端对象有一些有用的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;heartbeat&lt;/strong&gt;： 返回纳秒级的心跳，用于确保客户端保持连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;reset&lt;/strong&gt;：  清空并完全重置数据库，这是&lt;strong&gt;破坏性的，不可逆转的&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;client.heartbeat()
client.reset()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;客户端-服务端模式&lt;/h3&gt;
&lt;p&gt;Chroma 也可以配置为以客户端/服务器模式运行，在此模式下，Chroma 客户端连接到在单独进程中运行的 Chroma 服务器。&lt;/p&gt;
&lt;p&gt;要启动 Chroma 服务器，请运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chroma run --path /db_path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 Chroma HttpClient 连接到服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import chromadb

chroma_client = chromadb.HttpClient(host=&apos;localhost&apos;, port=8000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非常简单的就可以转换成客户端/服务端模式，Chroma 还提供异步 HTTP 客户端，行为和方法签名与同步客户端相同，但是所有会阻塞的方法现在都是异步的，使用它要调用 AsyncHttpClient：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import chromadb

async def main():
    client = await chromadb.AsyncHttpClient()

    collection = await client.create_collection(name=&quot;my_collection&quot;)
    await collection.add(
        documents=[&quot;hello world&quot;],
        ids=[&quot;id1&quot;]
    )

asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;云客户端&lt;/h3&gt;
&lt;p&gt;你可以使用 CloudClient 创建连接到 Chroma Cloud 的客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client = chromadb.CloudClient(
    tenant=&apos;Tenant ID&apos;,
    database=&apos;Database name&apos;,
    api_key=&apos;Chroma Cloud API key&apos;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你设置了 &lt;strong&gt;CHROMA_API_KEY&lt;/strong&gt;， &lt;strong&gt;CHROMA_TENANT&lt;/strong&gt; 和 &lt;strong&gt;CHROMA_DATABASE&lt;/strong&gt; 环境变量，你可以简单地实例化一个不带参数的 CloudClient：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client = chromadb.CloudClient()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;管理集合&lt;/h2&gt;
&lt;p&gt;Chroma 允许您使用&lt;strong&gt;集合原语&lt;/strong&gt;来管理嵌入集合，集合是 Chroma 中存储和查询的基本单位。&lt;/p&gt;
&lt;h3&gt;创建集合&lt;/h3&gt;
&lt;p&gt;Chroma 集合创建时需要指定名称，集合名称会在 URL 中使用，因此有一些限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;名称的长度必须介于 3 到 63 个字符之间。&lt;/li&gt;
&lt;li&gt;名称必须以小写字母或数字开头和结尾，中间可以包含点、破折号和下划线。&lt;/li&gt;
&lt;li&gt;名称不得包含两个连续的点。&lt;/li&gt;
&lt;li&gt;该名称不能是有效的 IP 地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;collection = client.create_collection(name=&quot;my_collection&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，集合名称在数据库中必须是&lt;strong&gt;唯一的&lt;/strong&gt;，如果你尝试创建与现有集合同名的集合，则会引发异常。&lt;/p&gt;
&lt;h3&gt;集合元数据&lt;/h3&gt;
&lt;p&gt;在创建集合时，可以传递可选的 metadata 参数，以便向集合添加元数据。这对添加有关集合的一般信息时（如创建时间、集合中存储的数据的描述等）非常有用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datetime import datetime

collection = client.create_collection(
    name=&quot;my_collection&quot;,
    metadata={
        &quot;description&quot;: &quot;my first Chroma collection&quot;,
        &quot;created&quot;: str(datetime.now())
    }  
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;获取集合&lt;/h3&gt;
&lt;p&gt;get_collection 函数将根据名称从 Chroma 获取一个集合，它返回一个 Collection 对象，包含名称、元数据、配置和 embedding_function。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection = client.get_collection(name=&quot;my-collection&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;get_or_create_collection 函数的行为类似，但是如果集合不存在，它将创建该集合。您可以向它传递 create_collection 函数期望的相同参数，如果集合已经存在，客户端会忽略它们。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection = client.get_or_create_collection(
    name=&quot;my-collection&quot;,
    metadata={&quot;description&quot;: &quot;...&quot;}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;list_collections 函数返回 Chroma 数据库中的集合列表，这些集合将按创建时间从最旧到最新排序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collections = client.list_collections()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，list_collections 最多返回 100 个集合。如果你有超过 100 个集合，或者只需要获取某一部分集合，你可以使用 limit 和 offset 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# get the first 100 collections
first_collections_batch = client.list_collections(limit=100)
# get the next 100 collections
second_collections_batch = client.list_collections(limit=100, offset=100)
# get 20 collections starting from the 50th
collections_subset = client.list_collections(limit=20, offset=50)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新版本的 Chroma 会将你用于创建集合的嵌入函数存储在服务器上，以便客户端可以在后续的“获取”操作中解析该函数。如果你运行的是旧版本的 Chroma 客户端或服务器（&amp;lt;1.1.13），则需要在使用 get_collection 函数时提供和创建集合时相同的嵌入函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection = client.get_collection( 
    name=&apos;my-collection&apos;,
    embedding_function=ef
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改集合&lt;/h3&gt;
&lt;p&gt;创建集合后，可以使用 modify 方法修改集合的名称、元数据和索引配置的元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.modify(
   name=&quot;new-name&quot;,
   metadata={&quot;description&quot;: &quot;new description&quot;} 
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;删除集合&lt;/h3&gt;
&lt;p&gt;你可以按名称删除集合，此操作将删除集合以及其所有的嵌入内容、相关文档和记录的元数据，删除集合是&lt;strong&gt;破坏性的，不可逆的&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.delete(name=&quot;my-collection&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;便捷方法&lt;/h3&gt;
&lt;p&gt;集合还提供了一些有用的便捷方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.count()
collection.peek()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;count&lt;/strong&gt;：返回集合中记录的数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;peek&lt;/strong&gt;：返回集合中的前10条记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;Chroma Collection 具有一个配置，该配置决定如何构造和使用嵌入索引，对于这些索引配置，通常使用默认值就可以为多数用例提供出色的性能。&lt;/p&gt;
&lt;p&gt;集合的嵌入函数也包含在配置中，在创建集合时，可以针对不同的数据、准确性和性能需求定制这些索引配置项，一些查询时配置也可以在集合创建后使用 modify 函数进行修改。&lt;/p&gt;
&lt;h3&gt;HNSW 索引&lt;/h3&gt;
&lt;p&gt;在单节点部署模式下的集合中，Chroma 使用 HNSW （Hierarchical Navigable Small World）索引来执行近似最近邻（ANN）搜索。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HNSW（分层可导航小世界）索引是一种基于图的数据结构，旨在在高维向量空间中进行高效的近似最近邻搜索。它的工作原理是构建一个多层图，其中每层包含数据点的子集，层级越高，数据越稀疏，如同“高速公路”一样，导航速度越快。该算法在每层相邻点之间建立连接，从而创建“小世界”属性，从而实现高效的搜索复杂度。在搜索过程中，算法从顶层开始，导航至嵌入空间中的查询点，然后向下移动，逐层优化搜索，直到找到最终的最近邻。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;HNSW 索引参数包括：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;space&lt;/strong&gt;： 定义嵌入向量空间的距离函数，从而定义相似性。默认值是 &lt;strong&gt;l2&lt;/strong&gt;（平方 l2 范式），其他可能的值是 &lt;strong&gt;cosine&lt;/strong&gt;（余弦相似度）和 &lt;strong&gt;ip&lt;/strong&gt;（内积）。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;space&lt;/th&gt;
&lt;th&gt;公式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;l2&lt;/td&gt;
&lt;td&gt;$d = \sum_{i=1}^{n} (A_i - B_i)^2$&lt;/td&gt;
&lt;td&gt;测量向量之间的绝对几何距离，适用于想要真正的空间接近&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ip&lt;/td&gt;
&lt;td&gt;$d = 1.0-\sum_{i=1}^{n} A_i B_i$&lt;/td&gt;
&lt;td&gt;关注向量对齐和大小，通常用于推荐系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cosine&lt;/td&gt;
&lt;td&gt;$d =1.0- \frac{\sum_{i=1}^{n} A_i B_i}{\sqrt{\sum_{i=1}^{n} A_i^2} \sqrt{\sum_{i=1}^{n} B_i^2}}$&lt;/td&gt;
&lt;td&gt;仅测量矢量之间的角度（忽略大小），非常适合文本嵌入的情况&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;应该确保你选择的 &lt;strong&gt;space&lt;/strong&gt; 受到集合的嵌入函数的支持，每个 Chroma 嵌入函数需要指定其默认空间和受支持的空间列表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;ef_construction&lt;/strong&gt;：确定了在&lt;strong&gt;创建索引时&lt;/strong&gt;用于选择邻居的候选列表的大小，值越高，索引质量越好，但会消耗更多的内存和时间，而值越低，索引构建速度越快，精度却会降低，默认值为 100。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ef_search&lt;/strong&gt;：确定了&lt;strong&gt;搜索最近邻居时&lt;/strong&gt;使用的动态候选列表的大小，较高的值通过探索更多的潜在邻居来提高召回率和准确性，但会增加查询时间和计算成本，而较低的值会导致更快但较不准确的搜索，缺省值是 100，该字段可以在创建完成后修改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;max_neighbors&lt;/strong&gt;：是在&lt;strong&gt;构建索引期间&lt;/strong&gt;图中的每个节点可以拥有的最大邻居（连接）数，较高的值会产生更密集的图，从而在搜索期间提高召回率和准确性，但会增加内存使用和构建时间，较低的值创建更稀疏的图，减少内存使用和构建时间，但代价是较低的搜索准确性和召回率，默认值为 16。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;num_threads&lt;/strong&gt;：指定了在索引构造或搜索操作期间使用的线程数，默认值是 multiprocessing.cpu_count()（可用的CPU核数），该字段创建后可以修改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;batch_size&lt;/strong&gt;：控制了索引操作期间每个批次要处理的向量的数目，默认值为 100，该字段创建后可以修改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sync_threshold&lt;/strong&gt;：确定何时将索引与持久存储同步，缺省值为 1000，该字段创建后可以修改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;resize_factor&lt;/strong&gt;：控制索引需要调整大小时的增长幅度。缺省值为 1.2，该字段创建后可以修改。&lt;/p&gt;
&lt;p&gt;例如，我们在这里创建一个具有自定义值的集合：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection = client.create_collection(
    name=&quot;my-collection&quot;,
    embedding_function=OpenAIEmbeddingFunction(model_name=&quot;text-embedding-3-small&quot;),
    configuration={
        &quot;hnsw&quot;: {
            &quot;space&quot;: &quot;cosine&quot;,
            &quot;ef_construction&quot;: 200
        }
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SPANN 索引&lt;/h3&gt;
&lt;p&gt;在分布式 Chroma 和 Chroma Cloud 集合中，我们使用 SPANN（空间近似最近邻）索引来执行近似最近邻 (ANN) 搜索。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SPANN 索引是一种数据结构，用于在大量高维向量中高效地查找近似最近邻。它的工作原理是将向量集划分为多个宽泛的簇（这样我们就可以在搜索过程中忽略大部分数据），然后在每个簇内构建高效、较小的索引，以实现快速的本地查找。这种两级方法有助于减少内存使用和搜索时间，即使在分布式系统中，搜索存储在硬盘或独立机器上的数十亿个向量也变得切实可行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;目前版本不允许自定义或修改 SPANN 配置，如果你设置了这些值，它们将被服务器忽略。&lt;/p&gt;
&lt;h3&gt;嵌入函数配置&lt;/h3&gt;
&lt;p&gt;在创建集合时选择的嵌入函数以及在实例化集合时使用的参数会持久化保存在集合的配置中，这使 Chroma 能够在你跨客户端使用集合时正确地重建它们。&lt;/p&gt;
&lt;p&gt;你可以将嵌入函数作为 create_collection 方法的参数，也可以直接在配置中设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction

# Using the `embedding_function` argument
openai_collection = client.create_collection(
    name=&quot;my_openai_collection&quot;,
    embedding_function=OpenAIEmbeddingFunction(
        model_name=&quot;text-embedding-3-small&quot;
    ),
    configuration={&quot;hnsw&quot;: {&quot;space&quot;: &quot;cosine&quot;}}
)

# Setting `embedding_function` in the collection&apos;s `configuration`
cohere_collection = client.get_or_create_collection(
    name=&quot;my_cohere_collection&quot;,
    configuration={
        &quot;embedding_function&quot;: OpenAIEmbeddingFunction(
        model_name=&quot;text-embedding-3-small&quot;
    	),
        &quot;hnsw&quot;: {&quot;space&quot;: &quot;cosine&quot;}
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：许多嵌入函数需要 API 密钥来与第三方提供者接口交互，Chroma 嵌入函数将自动查找用于存储第三方提供者 API 密钥的标准环境变量。例如 OpenAIEmbeddingFunction 将其 api_key 参数设置为 &lt;strong&gt;OPENAI_API_KEY&lt;/strong&gt; 环境变量的值。&lt;/p&gt;
&lt;h2&gt;管理数据&lt;/h2&gt;
&lt;h3&gt;添加数据&lt;/h3&gt;
&lt;p&gt;使用 add 方法将数据添加到 Chroma 集合中，它接受一个唯一字符串 id 列表和一个文档列表，Chroma 将使用集合的嵌入函数嵌入这些文档，它还将存储文档本身，还可以选择为添加的每个文档提供元数据字典。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.add(
    ids=[&quot;id1&quot;, &quot;id2&quot;, &quot;id3&quot;, ...],
    documents=[&quot;lorem ipsum...&quot;, &quot;doc2&quot;, &quot;doc3&quot;, ...],
    metadatas=[{&quot;chapter&quot;: 3, &quot;verse&quot;: 16}, {&quot;chapter&quot;: 3, &quot;verse&quot;: 5}, {&quot;chapter&quot;: 29, &quot;verse&quot;: 11}, ...],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果添加的记录具有集合中已经存在的 id，则该记录将被忽略，并且不会引发异常，这意味着如果批量添加操作失败，你可以安全地再次运行它。&lt;/p&gt;
&lt;p&gt;或者，可以直接提供与文档相关的嵌入列表，Chroma 将存储相关的文档，而不会再次嵌入。注意，在这种情况下，不能保证嵌入映射到与其关联的文档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.add(
    ids=[&quot;id1&quot;, &quot;id2&quot;, &quot;id3&quot;, ...],
    embeddings=[[1.1, 2.3, 3.2], [4.5, 6.9, 4.4], [1.1, 2.3, 3.2], ...],
    documents=[&quot;doc1&quot;, &quot;doc2&quot;, &quot;doc3&quot;, ...],
    metadatas=[{&quot;chapter&quot;: 3, &quot;verse&quot;: 16}, {&quot;chapter&quot;: 3, &quot;verse&quot;: 5}, {&quot;chapter&quot;: 29, &quot;verse&quot;: 11}, ...],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果提供的嵌入向量与集合中已索引的嵌入的维度不同，则会引发异常。&lt;/p&gt;
&lt;p&gt;另一种方案是可以将文档存储在其他地方，只需要向 Chroma 提供嵌入列表和元数据，可以使用 id 将嵌入向量与存储在其他地方的文档关联起来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.add(
    embeddings=[[1.1, 2.3, 3.2], [4.5, 6.9, 4.4], [1.1, 2.3, 3.2], ...],
    metadatas=[{&quot;chapter&quot;: 3, &quot;verse&quot;: 16}, {&quot;chapter&quot;: 3, &quot;verse&quot;: 5}, {&quot;chapter&quot;: 29, &quot;verse&quot;: 11}, ...],
    ids=[&quot;id1&quot;, &quot;id2&quot;, &quot;id3&quot;, ...]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更新数据&lt;/h3&gt;
&lt;p&gt;集合中记录的任何属性都可以使用 update 进行更新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.update(
    ids=[&quot;id1&quot;, &quot;id2&quot;, &quot;id3&quot;, ...],
    embeddings=[[1.1, 2.3, 3.2], [4.5, 6.9, 4.4], [1.1, 2.3, 3.2], ...],
    metadatas=[{&quot;chapter&quot;: 3, &quot;verse&quot;: 16}, {&quot;chapter&quot;: 3, &quot;verse&quot;: 5}, {&quot;chapter&quot;: 29, &quot;verse&quot;: 11}, ...],
    documents=[&quot;doc1&quot;, &quot;doc2&quot;, &quot;doc3&quot;, ...],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在集合中没有找到 id，则将记录错误并忽略更新，如果提供的文档没有相应的嵌入，则将使用集合的嵌入函数重新计算嵌入，如果提供的嵌入与集合的维度不同，则会引发异常，Chroma 还支持 upsert 操作，该操作可以更新现有记录，或者添加尚未存在的记录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.upsert(
    ids=[&quot;id1&quot;, &quot;id2&quot;, &quot;id3&quot;, ...],
    embeddings=[[1.1, 2.3, 3.2], [4.5, 6.9, 4.4], [1.1, 2.3, 3.2], ...],
    metadatas=[{&quot;chapter&quot;: 3, &quot;verse&quot;: 16}, {&quot;chapter&quot;: 3, &quot;verse&quot;: 5}, {&quot;chapter&quot;: 29, &quot;verse&quot;: 11}, ...],
    documents=[&quot;doc1&quot;, &quot;doc2&quot;, &quot;doc3&quot;, ...],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 id 不存在于集合中，将添加该条记录，如果 id 已经存在，则更新现有记录。&lt;/p&gt;
&lt;h3&gt;删除数据&lt;/h3&gt;
&lt;p&gt;Chroma 支持使用 delete 通过 id 删除集合中的记录，与记录关联的嵌入、文档和元数据将被删除，当然，这是一个破坏性的操作，无法撤销。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.delete(
    ids=[&quot;id1&quot;, &quot;id2&quot;, &quot;id3&quot;,...],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;delete 方法还支持 where 过滤器，如果没有提供 id，它将删除集合中与 where 过滤器匹配的所有项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.delete(
    ids=[&quot;id1&quot;, &quot;id2&quot;, &quot;id3&quot;,...],
	where={&quot;chapter&quot;: &quot;20&quot;}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询集合&lt;/h2&gt;
&lt;h3&gt;查询相似度&lt;/h3&gt;
&lt;p&gt;可以使用 query 方法在一个 Chroma 集合中运行相似度搜索：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_texts=[&quot;thus spake zarathustra&quot;, &quot;the oracle speaks&quot;]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chroma 将使用集合的嵌入函数来嵌入查询文本，并使用输出的嵌入向量在集合中运行向量相似性搜索。&lt;/p&gt;
&lt;p&gt;你也可以直接提供需要查询的嵌入向量，而不是提供 query_texts，如果你直接将嵌入向量添加到集合中，而不是使用嵌入函数，那么你需要直接使用嵌入向量来查询。如果提供的查询嵌入与集合中的嵌入具有不相同的维度，则会引发异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_embeddings=[[11.1, 12.1, 13.1],[1.1, 2.3, 3.2], ...]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，Chroma 相似度查询返回 10 个结果，你可以使用 n_results 参数修改这个数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_embeddings=[[11.1, 12.1, 13.1],[1.1, 2.3, 3.2], ...],
    n_results=5
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ids 参数允许将搜索范围限制在所提供 id 列表记录范围内：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_embeddings=[[11.1, 12.1, 13.1],[1.1, 2.3, 3.2], ...],
    n_results=5,
    ids=[&quot;id1&quot;, &quot;id2&quot;]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还可以使用 get 方法从集合中获取记录，它支持以下参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ids&lt;/strong&gt; ：从此列表中获取指定 id 的记录，如果未提供，则将按照添加到集合的顺序检索前 100 条记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;limit&lt;/strong&gt;：获取记录的数量，缺省值是 100。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;offset&lt;/strong&gt;：返回结果的起始偏移量，默认值为 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;collection.get(ids=[&quot;id1&quot;, &quot;ids2&quot;, ...])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;query 和 get 都有用于元数据过滤的 where 参数和用于全文搜索并且支持正则的 where_document 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_embeddings=[[11.1, 12.1, 13.1],[1.1, 2.3, 3.2], ...],
    n_results=5,
    where={&quot;page&quot;: 10}, 
    where_document={&quot;$contains&quot;: &quot;search string&quot;}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chroma 以列表的形式返回 query 和 get 的结果，结果对象是一个与 query 或 get 条件匹配的记录的 id 、嵌入、文档和元数据的列表，嵌入以 2 维 numpy 数组的形式返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class QueryResult(TypedDict):
    ids: List[IDs]
    embeddings: Optional[List[Embeddings]],
    documents: Optional[List[List[Document]]]
    metadatas: Optional[List[List[Metadata]]]
    distances: Optional[List[List[float]]]
    included: Include

class GetResult(TypedDict):
    ids: List[ID]
    embeddings: Optional[Embeddings],
    documents: Optional[List[Document]],
    metadatas: Optional[List[Metadata]]
    included: Include
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;query 的查询结果中还包含距离列表，这些是每个结果与输入查询的距离，查询结果也会根据输入的每个查询进行索引，例如下面，&lt;code&gt;results[&quot;ids&quot;][0]&lt;/code&gt;是第一个输入查询结果的记录 id 列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;results = collection.query(query_texts=[&quot;first query&quot;, &quot;second query&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，query 和 get 总是返回文档和元数据，也可以使用 include 参数指定要返回的内容，ids 是一定会返回的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# &apos;ids&apos;, &apos;documents&apos;, and &apos;metadatas&apos; are returned
collection.query(query_texts=[&quot;my query&quot;])

# Only &apos;ids&apos; and &apos;documents&apos; are returned
collection.get(include=[&quot;documents&quot;])

# &apos;ids&apos;, &apos;documents&apos;, &apos;metadatas&apos;, and &apos;embeddings&apos; are returned
collection.query(
    query_texts=[&quot;my query&quot;],
    include=[&quot;documents&quot;, &quot;metadatas&quot;, &quot;embeddings&quot;]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;元数据过滤&lt;/h3&gt;
&lt;p&gt;get 和 query 中的 where 参数用于根据元数据过滤记录，例如，在这个查询操作中，Chroma 只查询元数据字段  page 值为 10 的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_texts=[&quot;first query&quot;, &quot;second query&quot;],
    where={&quot;page&quot;: 10}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了对元数据进行过滤，必须为查询提供 where 过滤字典，字典必须是以下结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;metadata_field&quot;: {
        &amp;lt;Operator&amp;gt;: &amp;lt;Value&amp;gt;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 $eq 操作符相当于直接在 where 字段中筛选：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;metadata_field&quot;: &quot;search_string&quot;
}
# is equivalent to
{
    &quot;metadata_field&quot;: {
        &quot;$eq&quot;: &quot;search_string&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，这里我们查询所有 page 元数据字段大于 10 的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_texts=[&quot;first query&quot;, &quot;second query&quot;],
    where={&quot;page&quot;: { &quot;$gt&quot;: 10 }}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还可以使用逻辑操作符 and 和 or 来组合多个过滤器，and 操作符将返回与列表中所有过滤器匹配的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;$and&quot;: [
        {
            &quot;metadata_field&quot;: {
                &amp;lt;Operator&amp;gt;: &amp;lt;Value&amp;gt;
            }
        },
        {
            &quot;metadata_field&quot;: {
                &amp;lt;Operator&amp;gt;: &amp;lt;Value&amp;gt;
            }
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，这里我们查询所有 page 元数据字段在 5 到 10 之间的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_texts=[&quot;first query&quot;, &quot;second query&quot;],
    where={
        &quot;$and&quot;: [
            {&quot;page&quot;: {&quot;$gte&quot;: 5 }},
            {&quot;page&quot;: {&quot;$lte&quot;: 10 }},
        ]
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or 运算符将返回与列表中任何过滤器匹配的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;$or&quot;: [
        {
            &quot;metadata_field&quot;: {
                &amp;lt;Operator&amp;gt;: &amp;lt;Value&amp;gt;
            }
        },
        {
            &quot;metadata_field&quot;: {
                &amp;lt;Operator&amp;gt;: &amp;lt;Value&amp;gt;
            }
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，这里查询所有 color 元数据字段是 red 或 blue 的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.get(
    where={
        &quot;$or&quot;: [
            {&quot;color&quot;: &quot;red&quot;},
            {&quot;color&quot;: &quot;blue&quot;},
        ]
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;元数据过滤还支持以下包含运算符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;in&lt;/strong&gt;：值位于预定义列表中（字符串、整数、浮点数、布尔值）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nin&lt;/strong&gt;：值不在预定义列表中（字符串、整数、浮点数、布尔值）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;in 运算符将返回元数据属性属于所提供列表的一部分的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;metadata_field&quot;: {
    &quot;$in&quot;: [&quot;value1&quot;, &quot;value2&quot;, &quot;value3&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nin 运算符将返回元数据属性不属于所提供列表（或属性的键不存在）的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;metadata_field&quot;: {
    &quot;$nin&quot;: [&quot;value1&quot;, &quot;value2&quot;, &quot;value3&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，这里将获得所有 author 元数据字段值在可能值列表中的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.get(
    where={
       &quot;author&quot;: {&quot;$in&quot;: [&quot;Rowling&quot;, &quot;Fitzgerald&quot;, &quot;Herbert&quot;]}
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;get 和 query 可以处理元数据过滤和文档搜索：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_texts=[&quot;doc10&quot;, &quot;thus spake zarathustra&quot;, ...],
    n_results=10,
    where={&quot;metadata_field&quot;: &quot;is_equal_to_this&quot;},
    where_document={&quot;$contains&quot;:&quot;search_string&quot;}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;全文搜索和正则&lt;/h2&gt;
&lt;p&gt;get 和 query 中的 where_document 参数用于根据文档内容过滤记录，Chroma 支持使用 contains 和 not_contains 操作符进行全文搜索，还支持使用 regex 和 not_regex 操作符进行正则表达式匹配。&lt;/p&gt;
&lt;p&gt;例如，这里我们获取文档包含搜索字符串的所有记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.get(
   where_document={&quot;$contains&quot;: &quot;search string&quot;}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：全文搜索区分大小写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在这里，我们得到与电子邮件地址的正则表达式匹配的所有记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.get(
   where_document={
       &quot;$regex&quot;: &quot;^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$&quot;
   }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你还可以使用逻辑操作符 and 和 or 来组合多个过滤器，and 操作符将返回匹配列表中所有过滤器的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_texts=[&quot;query1&quot;, &quot;query2&quot;],
    where_document={
        &quot;$and&quot;: [
            {&quot;$contains&quot;: &quot;search_string_1&quot;},
            {&quot;$regex&quot;: &quot;[a-z]+&quot;},
        ]
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or 运算符将返回与列表中任何过滤器匹配的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collection.query(
    query_texts=[&quot;query1&quot;, &quot;query2&quot;],
    where_document={
        &quot;$or&quot;: [
            {&quot;$contains&quot;: &quot;search_string_1&quot;},
            {&quot;$not_contains&quot;: &quot;search_string_2&quot;},
        ]
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;嵌入函数&lt;/h2&gt;
&lt;h3&gt;默认嵌入函数&lt;/h3&gt;
&lt;p&gt;Chroma 的默认嵌入功能使用 sentence-transformers/all-MiniLM-L6-v2 模型来创建嵌入，这个嵌入模型可以创建句子和文档嵌入，当这个嵌入函数在你的机器上运行时，会自动下载模型文件。&lt;/p&gt;
&lt;p&gt;如果你在创建集合时没有指定嵌入函数，Chroma 会将其设置为 DefaultEmbeddingFunction。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from chromadb.utils.embedding_functions import DefaultEmbeddingFunction

default_ef = DefaultEmbeddingFunction()
embeddings = default_ef([&quot;foo&quot;])
print(embeddings) # [[0.05035809800028801, 0.0626462921500206, -0.061827320605516434...]]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;collection = client.create_collection(name=&quot;my_collection&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用嵌入函数&lt;/h3&gt;
&lt;p&gt;Chroma 为流行的嵌入提供商提供了轻量级的包装器，使其易于在应用程序中使用，你可以在创建 Chroma Collection 时设置嵌入函数，以便在添加和查询数据时自动使用，或者你可以直接调用它们。&lt;/p&gt;
&lt;p&gt;以 huggingface 为例，Chroma 提供了便捷的 API 封装，嵌入模型运行在 huggingface 服务器上，只需要提供 API 密钥，就可以直接使用嵌入函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import chromadb.utils.embedding_functions as embedding_functions
huggingface_ef = embedding_functions.HuggingFaceEmbeddingFunction(
    api_key=&quot;YOUR_API_KEY&quot;,
    model_name=&quot;sentence-transformers/all-MiniLM-L6-v2&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义嵌入&lt;/h3&gt;
&lt;p&gt;你可以创建自己的嵌入函数来使用 Chroma，它只需要实现 EmbeddingFunction。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from chromadb import Documents, EmbeddingFunction, Embeddings

class MyEmbeddingFunction(EmbeddingFunction):
    def __call__(self, input: Documents) -&amp;gt; Embeddings:
        # embed the documents somehow
        return embeddings
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;能看到这里的人真的非常有耐心，文章有点长，但这不是有关 Chroma 的全部内容，但是包括了大部分重要的知识点，需要深入学习的朋友可以看 &lt;a href=&quot;https://docs.trychroma.com/docs/overview/introduction&quot;&gt;Chroma Docs&lt;/a&gt; 和 &lt;a href=&quot;https://cookbook.chromadb.dev/&quot;&gt;Chroma Cookbook&lt;/a&gt; ，文章内容采用谷歌翻译、有道翻译和英文原文对比，在基于自己的理解得出较准确通顺的最终版本，因为水平有效，如果有内容上的错误请留言指正。&lt;/p&gt;
</content:encoded><category>AI</category><author>Teclado</author></item><item><title>向量数据库和索引技术</title><link>https://blog.teclado.cn/posts/vector_database/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/vector_database/</guid><pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在之前介绍过了什么是 embedding 以及在构建 RAG 系统时如何选择合适的 embedding 模型，当我们将原始数据嵌入成 embedding 向量后，为了能重复使用这些向量数据，我们就需要一个专门用于向量存储的数据库——向量数据库（vector database）。&lt;/p&gt;
&lt;h2&gt;什么是向量数据库&lt;/h2&gt;
&lt;p&gt;向量数据库是一种专门用于存储、索引和查询向量数据的数据库系统，它与其他传统数据库的核心差异在于数据表示方式和查询逻辑。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;传统数据库&lt;/th&gt;
&lt;th&gt;向量数据库&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据存储&lt;/td&gt;
&lt;td&gt;结构化数据（数值、字符串、日期等）&lt;/td&gt;
&lt;td&gt;高维向量（浮点型数组）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询方式&lt;/td&gt;
&lt;td&gt;精确匹配（如 where name = &apos;cat&apos;）&lt;/td&gt;
&lt;td&gt;相似性搜索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引方式&lt;/td&gt;
&lt;td&gt;B+ 树、哈希索引等&lt;/td&gt;
&lt;td&gt;ANN 索引（如 HNSW、IVF）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于向量数据库存在一个普遍的误解，认为向量数据库只是对于近似最近邻（ANN）搜索算法的封装，从本质上来说向量数据库是对向量数据管理和查询的一个综合解决方案，比起单独的构建向量索引的工具，向量数据库在云原生、多租户、可扩展性以及安全措施上有着更多的功能。&lt;/p&gt;
&lt;h2&gt;向量数据库的工作原理&lt;/h2&gt;
&lt;p&gt;在传统数据库中，查询需要返回数据表中所有符合匹配条件的数据，而在向量数据库中，查询是通过计算向量数据之间的相似性度量，来返回与查询向量最接近的向量数据，向量数据库使用的查询算法都属于是近似最近邻（ANN）算法，由于查询返回的是近似结果，我们主要考虑的是在准确性和查询速度之间进行权衡，结果越准确，查询速度也就越慢，所以需要找到一个准确性和速度平衡的点。&lt;/p&gt;
&lt;p&gt;向量数据库的常见工作流程是索引、查询、后处理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;向量数据库的工作流程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;索引&lt;/strong&gt;：向量数据库使用诸如 HNSW、IVF、PG 等算法对向量数据建立索引，这一步操作将向量数据映射成一种能够快速查询的数据结构。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;查询&lt;/strong&gt;：向量数据库将索引后的查询向量与数据库中的索引向量比较，找到最近邻向量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后处理&lt;/strong&gt;：在某些情况下，需要对查询到的最近邻向量进行处理返回最终结果，例如使用不同相似性度量进行重新排序。&lt;/p&gt;
&lt;h2&gt;索引技术分类&lt;/h2&gt;
&lt;p&gt;基于树的方法对于低维数据非常有效，并且可以提供精确的最近邻搜索。然而，由于“维度诅咒”，它们在高维空间中的性能通常会下降。此外，它们需要大量内存，对于大型数据集效率较低，这会导致构建时间更长和延迟更高。&lt;/p&gt;
&lt;p&gt;量化方法在内存利用上效率较高，通过将向量压缩为紧凑的代码来实现快速搜索。但是，这种压缩可能会导致信息丢失，从而降低搜索准确性。另外，这些方法在训练阶段的计算成本较高，会增加构建时间。&lt;/p&gt;
&lt;p&gt;哈希方法速度快且相对节省内存，它将相似的向量映射到同一个哈希桶中。在处理高维数据和大规模数据集时表现良好，具有较高的吞吐量。然而，由于哈希冲突可能会导致误报和漏报，从而降低搜索结果的质量。选择合适数量的哈希函数和哈希表至关重要，因为它们会显著影响性能。&lt;/p&gt;
&lt;p&gt;聚类方法可以通过将搜索空间缩小到特定的聚类来加快搜索操作，但搜索结果的准确性可能会受到聚类质量的影响。聚类通常是一个批处理过程，这意味着它不太适合不断添加新向量的动态数据，因为这会导致频繁的重新索引。&lt;/p&gt;
&lt;p&gt;基于图的方法在准确性和速度之间取得了较好的平衡。它们对高维数据很有效，并且可以提供高质量的搜索结果。但是，由于需要存储图结构，它们可能会占用大量内存，而且图的构建在计算上也很昂贵。&lt;/p&gt;
&lt;p&gt;向量数据库中算法的选择取决于任务的具体要求，包括数据集的大小和维度、可用的计算资源，以及在准确性和效率之间可接受的权衡。许多现代向量数据库使用混合方法，结合不同方法的优点来实现高速和高精度。如果希望充分发挥向量数据库性能，就必须要更加了解这些算法。&lt;/p&gt;
&lt;h2&gt;索引算法&lt;/h2&gt;
&lt;h3&gt;FLAT Index&lt;/h3&gt;
&lt;p&gt;FLAT Index 也称全表扫描，它不会对向量数据做任何修改，直接对数据库中的全部向量数据做相似性计算，返回 top-k 个近似值。全表扫描的搜索准确度可以说是 100%，但是搜索速度可是说是 0%，所以我们要采用近似最近邻（ANN）搜索，来找到一个准确度和速度的平衡。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./flat-index.png&quot; alt=&quot;FLAT Index&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;IVF Index&lt;/h3&gt;
&lt;p&gt;IVF（Inverted File Index）倒排文件索引，用 K-means 算法将所有向量分成 k 个块，每个块有一个块中心向量，为每个块内部建立倒排索引。当 Query 查询向量时先与各个块中心向量比较，找出与 Query 最近的 n 个块（如 5），然后只搜索这些块里的向量。通过聚类划分区域来缩小查询范围，可以牺牲少量精度，大大提升查询速度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./IVF-Index.png&quot; alt=&quot;IVF-Index&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;HNSW index&lt;/h3&gt;
&lt;p&gt;HNSW（Hierarchical Navigable Small World）的核心思想是通过构建一个分层的图结构来实现高效的近似最近邻搜索，所以首先需要分层（例如分 3 层），然后确定每层的数据数量，layer0 层是全部数据，从 layer0 层中随机抽取一部分数据到 layer1 层，layer2 层则从下一层 layer1 层中抽取一部分数据，为每一层的数据建立 k 近邻图结构。搜索时从顶层开始，找到与查询向量最接近的节点，然后逐层向下，在更精细的图中进行搜索，直到最底层。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./hnsw.jpg&quot; alt=&quot;hnsw&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;PG&lt;/h3&gt;
&lt;p&gt;PG（Product Quantization）量化乘积是一种高效的向量压缩技术，核心思想是将高维向量空间分解成多个低维子空间，然后对每个子空间进行聚类生成码本，每个子空间用聚类中心的索引来表示，原始向量压缩为低维子空间索引的组合。使用量化乘积可以大大降低内存占用（压缩率可达 10-100 倍），提高查询速度，量化过程会损失部分信息，所以需要平衡速度和精度。PQ 常与其他索引技术（如IVF）结合使用，形成 IVF-PQ 混合索引，在精度和效率间取得更好平衡。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pg.jpg&quot; alt=&quot;pg&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;相似性度量&lt;/h2&gt;
&lt;p&gt;在相似性搜索中，需要计算两个向量之间的距离，然后根据距离来判断它们的相似度。关于如何计算向量在高维空间中距离，常见的向量相似度算法有欧几里得距离、余弦相似度、点积相似度。&lt;/p&gt;
&lt;h3&gt;欧几里得距离&lt;/h3&gt;
&lt;p&gt;欧几里得距离是指两个向量之间的直线距离，它的计算公式为：
$$
d(\mathbf{A}, \mathbf{B})=\sqrt{\sum_{i=1}^{n}\left(A_{i}-B_{i}\right)^{2}}
$$
其中，$A$ 和 $B$ 分别表示两个向量，$n$ 表示向量的维度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./euclidean_distance.jpg&quot; alt=&quot;euclidean distance&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;余弦相似度&lt;/h3&gt;
&lt;p&gt;余弦相似度是指两个向量之间的夹角余弦值，它的计算公式为：&lt;/p&gt;
&lt;p&gt;$$
\text{cosine}(A,B) = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| |\mathbf{B}|}
$$
其中，$A$ 和 $B$ 分别表示两个向量，$\cdot$ 表示向量的点积，$|A|$ 和 $|B|$ 分别表示两个向量的模长。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cosine.jpg&quot; alt=&quot;cosine&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;点积相似度&lt;/h3&gt;
&lt;p&gt;向量的点积相似度是指两个向量之间的点积值，它的计算公式为：
$$
\text{dot}(A,B) = \mathbf{A} \cdot \mathbf{B} = \sum_{i=1}^{n} A_i B_i
$$
其中，$A$ 和 $B$ 分别表示两个向量，$n$ 表示向量的维度。
&lt;img src=&quot;./dot_product.jpg&quot; alt=&quot;dot_product&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;过滤（filtering）&lt;/h2&gt;
&lt;p&gt;在实际的业务场景中，往往不需要在整个向量数据库中进行相似性搜索，而是通过部分的业务字段进行过滤再进行查询。所以存储在数据库的向量往往还需要包含元数据，例如用户 ID、文档 ID 等信息。这样就可以在搜索的时候，根据元数据来过滤搜索结果，从而得到最终的结果。&lt;/p&gt;
&lt;p&gt;为此，向量数据库通常维护两个索引：一个是向量索引，另一个是元数据索引。
&lt;img src=&quot;./filtering.jpg&quot; alt=&quot;filtering&quot; /&gt;&lt;/p&gt;
&lt;p&gt;过滤过程可以在向量搜索本身之前或之后执行，但每种方法都有自己的挑战，可能会影响查询性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pre-filtering：在向量搜索之前进行元数据过滤。虽然这可以帮助减少搜索空间，但是元数据过滤可能会过滤掉和结果相关的数据。&lt;/li&gt;
&lt;li&gt;Post-filtering：在向量搜索完成后进行元数据过滤。考虑全部数据之后进行元数据过滤会增加很多开销，并且减慢查询速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了优化过滤流程，向量数据库使用各种技术，例如利用先进的索引方法来处理元数据或使用并行处理来加速过滤任务。平衡搜索性能和筛选精度之间的权衡对于提供高效且相关的向量数据库查询结果至关重要。&lt;/p&gt;
&lt;h2&gt;热门的向量数据库&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./databases.png&quot; alt=&quot;databases&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;如何选择向量数据库&lt;/h2&gt;
&lt;p&gt;如何选择向量数据库因具体项目需求、预算限制和个人偏好而异，下面是一些比较通用的结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果需要开源、可自托管且功能全面，可以选择 Milvus 或 Weaviate&lt;/li&gt;
&lt;li&gt;如果希望快速使用且不想管理基础设施，可以选择 Pinecone&lt;/li&gt;
&lt;li&gt;如果注重性能和内存效率，可以考虑 Qdrant&lt;/li&gt;
&lt;li&gt;如果项目需要与 LangChain 等框架集成，且希望轻量级，Chroma 是一个不错的选择&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://guangzhengli.com/blog/zh/vector-database&quot;&gt;https://guangzhengli.com/blog/zh/vector-database&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/27399676042&quot;&gt;https://zhuanlan.zhihu.com/p/27399676042&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>AI</category><author>Teclado</author></item><item><title>理解 embedding（嵌入）</title><link>https://blog.teclado.cn/posts/embedding-guide/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/embedding-guide/</guid><pubDate>Fri, 04 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 embedding&lt;/h2&gt;
&lt;p&gt;在大模型中，”embedding“指的是将某种类型的输入数据（如文本、图像、声音等）转换成一个稠密的数值向量的过程。这些向量通常包含较多纬度，每一个纬度代表输入数据的某种抽象特征和属性。embedding 的目的是将实际的输入转换为计算机能够更有效处理的向量格式。&lt;/p&gt;
&lt;p&gt;在自然语言处理（NLP）中通常使用 embedding 模型将文本数据转换成 embedding 向量，这些 embedding 向量捕获了文本的语义特征，在 embedding 向量空间中，语义相近的实体在向量空间中距离更近，语义不相近的实体在向量空间中距离更远。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./embedding%E7%9A%84%E7%9B%AE%E7%9A%84.png&quot; alt=&quot;embedding的目的&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于 embedding 模型有这种精确理解语义的能力，通过对于不同句子段落的语义相似度的计算，embedding 模型可以应用在检索增强生成（RAG）、推荐系统这些领域。&lt;/p&gt;
&lt;p&gt;注意：在后面学习 transformer 架构时会发现模型架构中会一个 embedding 层，这里的 embedding 层和 embedding 模型存在差异，核心区别是 embedding 层是服务于生成任务的内部零件，而 embedding 模型是专注于理解的最终完整产品。所以他们的目标、训练方式和优化方向完全不同。&lt;/p&gt;
&lt;h2&gt;embedding 的底层&lt;/h2&gt;
&lt;p&gt;当前主流的上下文嵌入模型（例如 BAAI/bge-large-zh-v1.5）底层基于 BERT 模型，而 BERT 模型是基于 Transformer Encoder-Only 架构。&lt;/p&gt;
&lt;p&gt;BERT 是针对于 NLU 任务打造的预训练模型，其输入一般是文本序列，而输出一般是 Label，例如情感分类的积极、消极 Label。但是，正如 Transformer 是一个 Seq2Seq 模型，使用 Encoder 堆叠而成的 BERT 本质上也是一个 Seq2Seq 模型，只是没有加入对特定任务的 Decoder，因此，为适配各种 NLU 任务，在模型的最顶层加入了一个分类头 prediction_heads，用于将多维度的隐藏状态通过线性层转换到分类维度（例如，如果一共有两个类别，prediction_heads 输出的就是两维向量）。&lt;/p&gt;
&lt;p&gt;输入的文本序列会首先通过 tokenizer（分词器） 转化成 input_ids（基本每一个模型在 tokenizer 的操作都类似，可以参考 Transformer 的 tokenizer 机制），然后进入 embedding 层转化为特定维度的 hidden_states，再经过 Encoder 块。Encoder 块中是对叠起来的 N 层 Encoder Layer，BERT 有两种规模的模型，分别是 base 版本（12层 Encoder Layer，768 的隐藏层维度，总参数量 110M），large 版本（24层 Encoder Layer，1024 的隐藏层维度，总参数量 340M）。通过 Encoder 编码之后的最顶层 hidden_states 最后经过 prediction_heads 就得到了最后的类别概率，经过 Softmax 计算就可以计算出模型预测的类别。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./BERT%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;BERT模型架构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BERT 更大的创新点在于提出了两个新的预训练任务上——MLM 和 NSP（Next Sentence Prediction，下一句预测），使用了预训练-微调范式将预训练和微调分离，完成一次预训练的模型可以仅通过微调应用在几乎所有下游任务上，所以只要微调的成本足够低，预训练的成本高一点也是有价值的。&lt;/p&gt;
&lt;p&gt;MLM 的思路是模拟“完形填空”，在一个文本序列中随机遮蔽部分 token，然后将所有未被遮蔽的 token 输入模型，要求模型根据输入预测被遮蔽的 token。例如，输入和输出可以是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：I &amp;lt;MASK&amp;gt; you because you are &amp;lt;MASK&amp;gt;
输出：&amp;lt;MASK&amp;gt; - love; &amp;lt;MASK&amp;gt; - wonderful
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模型可以利用被遮蔽的 token 的上文和下文一起理解语义来预测被遮蔽的 token，因此通过这样的任务，模型可以拟合双向语义，也就能够更好地实现文本的理解。同样，MLM 任务无需对文本进行任何人为的标注，只需要对文本进行随机遮蔽即可，因此也可以利用互联网所有文本语料实现预训练。例如，BERT 的预训练就使用了足足 3300M 单词的语料。&lt;/p&gt;
&lt;p&gt;NSP 的核心思想是针对句级的 NLU 任务，例如问答匹配、自然语言推理等。问答匹配是指，输入一个问题和若干个回答，要求模型找出问题的真正回答；自然语言推理是指，输入一个前提和一个推理，判断推理是否是符合前提的。这样的任务都需要模型在句级去拟合关系，判断两个句子之间的关系，而不仅是 MLM 在 token 级拟合的语义关系。因此，BERT 提出了 NSP 任务来训练模型在句级的语义关系拟合。&lt;/p&gt;
&lt;p&gt;NSP 任务的核心思路是要求模型判断一个句对的两个句子是否是连续的上下文。例如，输入和输入可以是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：
    Sentence A：I love you.
    Sentence B: Because you are wonderful.
输出：
    1（是连续上下文）

输入：
    Sentence A：I love you.
    Sentence B: Because today&apos;s dinner is so nice.
输出：
    0（不是连续上下文）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过要求模型判断句对关系，从而迫使模型拟合句子之间的关系，来适配句级的 NLU 任务。同样，由于 NSP 的正样本可以从无监督语料中随机抽取任意连续的句子，而负样本可以对句子打乱后随机抽取，因此也可以具有几乎无限量的训练数据。&lt;/p&gt;
&lt;h2&gt;如何挑选合适的 embedding&lt;/h2&gt;
&lt;p&gt;在构建 RAG（检索增强生成） 系统时，需要将用户提问（query）和外部知识库做内容相关性匹配，这里的内容相关性就是通过对 embedding 向量计算得到的，所以 embedding 的好坏决定的上下文的相关性，如果引入不相关的上下文，就会对 LLM 回答问题造成干扰，影响 RAG 系统的性能，所以如何挑选合适的 embedding 模型就是一个关键问题。&lt;/p&gt;
&lt;p&gt;HuggingFace 的 &lt;strong&gt;MTEB leaderboard&lt;/strong&gt; 是一个一站式的文本 embedding 模型评测榜，当你不知道什么模型适合自己的时候，参考 MTEB 榜单是一个不错的开始，根据项目场景的需求关注特定任务的性能得分，MTEB 包含以下9种任务类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Semantic Textual Similarity (STS) ：确定句子对的相似性。&lt;/li&gt;
&lt;li&gt;Summarization ：评估机器生成的摘要。&lt;/li&gt;
&lt;li&gt;Retrieval ：检索内容相关的文档。&lt;/li&gt;
&lt;li&gt;Reranking ：根据与查询的相关性对结果进行重新排序。&lt;/li&gt;
&lt;li&gt;Bitext Mining ：寻找两种语言句子集之间的最佳匹配。&lt;/li&gt;
&lt;li&gt;Classification ：使用嵌入模型训练逻辑回归分类器。&lt;/li&gt;
&lt;li&gt;Clustering ：将句子或段落分组为有意义的簇。&lt;/li&gt;
&lt;li&gt;Pair Classification ：对文本输入分配二元标签。&lt;/li&gt;
&lt;li&gt;Multilabel Classification: 对文本输入分配多元标签。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了模型排名外，还需要关注下面这些特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型大小：较大模型计算成本大，适合较复杂的任务。&lt;/li&gt;
&lt;li&gt;嵌入纬度：纬度低存储和比较更快，但是语义捕获能力降低。&lt;/li&gt;
&lt;li&gt;语言支持：多语言模型在跨语言场景下表现更好，单语言模型在特定语言上表现更好。&lt;/li&gt;
&lt;li&gt;推理时间：如果对响应速度有要求在需要选择推理速度更快的模型。&lt;/li&gt;
&lt;li&gt;长文本能力：需要考虑模型对文本块的 token 长度限制，超过限制的文本会被模型截断。&lt;/li&gt;
&lt;li&gt;成本和可扩展：开源模型免费支持微调，专有模型涉及 token 费用和隐私问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外不推荐使用 LLM 微调的 embedding 模型，这类模型大小过大，并且性能和 BERT 模型差不多，但是计算成本高很多。&lt;/p&gt;
&lt;p&gt;但是最近的 Qwen3/Qwen3-Embedding 模型很火，这个模型基于 Qwen3-base 模型训练，属于 LLM 架构，但是采用了混合专家（MoE）架构设计，针对 embedding 任务做了深度优化，虽然单次推理成本比 BERT 模型高，但在大规模检索系统中，Qwen3-Embedding 的高准确率可减少召回次数，综合成本效益反而提升。所以说 Qwen3-Embedding 选择基于LLM架构，并非简单追求模型规模，而是通过平衡精度-成本来完成更好的 embedding 效果，对于简单任务，BERT 仍是高性价比选择；但在需要泛化性、多语言支持或长文本处理的场景，LLM 衍生的嵌入模型也许会变成发展趋势。&lt;/p&gt;
&lt;p&gt;因为 MTEB 评测数据集是公开的，所以模型通常会基于评测数据进行效果优化，因此 MTEB 的排名和评分更适合作为参考，一般来说选择合适 embedding 模型的流程是根据自己的场景需求参考 MTEB 榜单挑选出一些备选模型，然后构建 baseline 和自己评测的数据集，然后对于 baseline 进行测试，根据测试的结果来选择是否要更换或者优化 embedding 模型，经过反复迭代才能找到适合自己的 embedding 模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%8C%91%E9%80%89embedding%E6%A8%A1%E5%9E%8B%E7%9A%84%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;挑选embedding模型的流程&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;embedding 的调用方式&lt;/h2&gt;
&lt;p&gt;这里选择 Qwen3/Qwen3-Embedding-0.6B 模型来演示，代码来自官方示例。&lt;/p&gt;
&lt;h3&gt;transformers&lt;/h3&gt;
&lt;p&gt;Qwen3-Embedding 系列模型是指令感知型嵌入模型，所以对于 query 必须要指定 task，而对于 document 则不需要指定 task，get_detailed_instruct()  这个功能函数就是将 query 和 task 绑定；Qwen3 模型处理数据使用了左填充（Left-Padding），序列的最后一个 token 通常都是有效的，在模型推理后最后一个 token 的向量包含整个序列的信息，last_token_pool() 这个功能函数的作用就是获取序列最后一个 token 向量作为最终的 embedding 向量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 导入要用到的库
import torch.nn.functional as F
from torch import Tensor
from transformers import AutoTokenizer, AutoModel

# 获取序列中最后一个 token 的推理向量
def last_token_pool(last_hidden_states: Tensor,
                 attention_mask: Tensor) -&amp;gt; Tensor:
    left_padding = (attention_mask[:, -1].sum() == attention_mask.shape[0])
    if left_padding:
        return last_hidden_states[:, -1]
    else:
        sequence_lengths = attention_mask.sum(dim=1) - 1
        batch_size = last_hidden_states.shape[0]
        return last_hidden_states[torch.arange(batch_size, device=last_hidden_states.device), sequence_lengths]

# 构建 task 和 query
def get_detailed_instruct(task_description: str, query: str) -&amp;gt; str:
    return f&apos;Instruct: {task_description}\nQuery:{query}&apos;

# 定义 Task、Query 和 Documents
task = &apos;Given a web search query, retrieve relevant passages that answer the query&apos;

queries = [
    get_detailed_instruct(task, &apos;What is the capital of China?&apos;),
    get_detailed_instruct(task, &apos;Explain gravity&apos;)
]

documents = [
    &quot;The capital of China is Beijing.&quot;,
    &quot;Gravity is a force that attracts two bodies towards each other. It gives weight to physical objects and is responsible for the movement of planets around the sun.&quot;
]
input_texts = queries + documents

# 加载模型和分词器
tokenizer = AutoTokenizer.from_pretrained(&apos;Qwen/Qwen3-Embedding-0.6B&apos;, padding_side=&apos;left&apos;)
model = AutoModel.from_pretrained(&apos;Qwen/Qwen3-Embedding-0.6B&apos;)

max_length = 8192

# 对输入文本进行分词
batch_dict = tokenizer(
    input_texts,
    padding=True,
    truncation=True,
    max_length=max_length,
    return_tensors=&quot;pt&quot;,
)
batch_dict.to(model.device)

# embedding
outputs = model(**batch_dict)
embeddings = last_token_pool(outputs.last_hidden_state, batch_dict[&apos;attention_mask&apos;])

# normalize embeddings
embeddings = F.normalize(embeddings, p=2, dim=1)
scores = (embeddings[:2] @ embeddings[2:].T)
print(scores.tolist())
# [[0.7645568251609802, 0.14142508804798126], [0.13549736142158508, 0.5999549627304077]]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;sentence-transformers&lt;/h3&gt;
&lt;p&gt;sentence-transformers 是 huggingface 推出的专门处理嵌入任务的 python 库，使用 sentence-transformers 可以减少很多工作，简化代码提高效率。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sentence_transformers import SentenceTransformer

# Load the model
model = SentenceTransformer(&quot;Qwen/Qwen3-Embedding-0.6B&quot;)

# The queries and documents to embed
queries = [
    &quot;What is the capital of China?&quot;,
    &quot;Explain gravity&quot;,
]
documents = [
    &quot;The capital of China is Beijing.&quot;,
    &quot;Gravity is a force that attracts two bodies towards each other. It gives weight to physical objects and is responsible for the movement of planets around the sun.&quot;,
]

# prompt_name 可以控制 task
query_embeddings = model.encode(queries, prompt_name=&quot;query&quot;)
document_embeddings = model.encode(documents)

# Compute the (cosine) similarity between the query and document embeddings
similarity = model.similarity(query_embeddings, document_embeddings)
print(similarity)
# tensor([[0.7646, 0.1414],
#         [0.1355, 0.6000]])
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>AI</category><author>Teclado</author></item><item><title>读《一句顶一万句》上篇《出延津记》</title><link>https://blog.teclado.cn/posts/leave-hometown/</link><guid isPermaLink="true">https://blog.teclado.cn/posts/leave-hometown/</guid><pubDate>Mon, 30 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;只来得及读上篇&lt;/h2&gt;
&lt;p&gt;上周花了一些时间读刘震云的《一句顶一万句》，书分上下两篇，全书将近27万字，这次读得比较快，花了14个小时读完上篇出延津记的十四章，为什么读得这么快呢，一个原因是微信读书可以免费借阅七天付费书，另一个原因是小说的剧情发展和人物刻画引人深思，让我止不住想要继续读下去。但快赶慢赶得读一周也只读完了上篇的内容，下篇的内容目前已经无法再读下去了。&lt;/p&gt;
&lt;h2&gt;各个庄地老字辈&lt;/h2&gt;
&lt;p&gt;书中刻画了很多人物，大多是底层百姓的形象，像是剃头的老裴，杀猪的老曾，赶车的老马，传教的老詹，每一个人物身上都有自己的故事，当然小说的主角是卖豆腐的老杨的第二个儿子杨百顺，来自杨家庄，在小说里作者在前期使用了大量的一个人物来自一个庄的写法，像是马家庄的老马，刘家庄的老刘，孔家庄的老孔这种写法，而且这类角色通常没名没姓，都称老字辈。&lt;/p&gt;
&lt;h2&gt;百业顺利&lt;/h2&gt;
&lt;p&gt;卖豆腐的老杨有三个儿子，大儿子杨百业，早期一直跟着老杨卖豆腐，不满老杨把他当做拉磨的驴，想要成家娶媳妇，后面娶得大户人家老秦家的千金，算是很好命了；二儿子杨百顺是故事的主角；三儿子杨百利，在老杨的算计下被送去新学念书，新学没办多久就取消了，原因是县长小韩下台了，杨百利在新学上和牛国兴一起玩起了空喷，新学结束后进入牛国兴的爹老牛的铸铁厂当保安，之后因为和牛国兴关系闹僵遇到喜欢聊天的铁路机务司采购老万，相谈甚欢最后被引荐去了火车上当司炉，虽是个铲煤的脏活累活但是在大哥的婚礼上正装出席谈笑风生，给当时正结束杀猪学徒生涯没去处的杨百顺相当大的打击。&lt;/p&gt;
&lt;h2&gt;百顺不顺&lt;/h2&gt;
&lt;p&gt;卖豆腐的老杨没什么主意，有什么事要找拉车的老马讨看法，不过三个儿子的名字连起来是百业顺利倒是有几分水准，可惜的是杨百顺这个名字取的一点也不应和，百顺百顺一点不顺，以至于到后面既丢了名字又丢了姓，从杨百顺到杨摩西，从杨摩西到吴摩西，再到后来的罗长礼，这一次次的打击都发生在了一个人二十五岁之前，如此的让人难以想象但又那么的真实，我想每个人都能从杨百顺身上看到自己看到无奈。&lt;/p&gt;
&lt;h2&gt;想刀人的心叫他人温暖&lt;/h2&gt;
&lt;p&gt;作者在小说里设计一处历史的重复来将剧情推向高潮，是小说中让人记忆最深刻的亮点之一，这两处是老裴和杨百顺的偶遇以及杨百顺和来喜的偶遇，都是杀人-偶遇-放弃的情节，这里用了相同的结构展示了杨百顺的成长经历，从害怕回家跟老裴去镇上吃面到自己酒后提刀去马家庄杀老马，同样的一个人同样的剧情安排，只是角色的位置已经不同。我想杨百顺到七十岁都记得老裴的救赎，记得这个好朋友在自己年幼无助的时候给予了可以治愈终生的援助；我想杨百顺也感谢来喜的出现，让自己没有因怒气上头取人性命，让自己后悔终生。&lt;/p&gt;
&lt;h2&gt;最好的信徒老詹&lt;/h2&gt;
&lt;p&gt;写到这里已经很晚了，但是还是想继续写一下老詹的故事，老詹被称为书中最纯粹的一个人，书中的每个人都有自己的小心思为了自己的利益得失而活，而只有老詹异国他乡传教五十年，虽只有八个信众但是直至死前还想着重修教堂给民众带来救赎。老詹是个意大利人，学过建筑，当初他叔是开封教会会长，他跟着他叔来中国传教，那时他还只有二十六岁，他叔给了他一笔津贴让他来延津开疆扩土，老詹来延津后用这笔钱设计扩建了一个大教堂开始传教，后面自从爱做家具的县长老胡退休后，新来的县长们都将教堂据为己有，老詹没了去处，只能住在破庙里。和老詹一起传教是他的徒弟小赵，小赵不是信徒，小赵是老詹雇来给自己骑自行车的，平日里载着老詹去各乡各庄传教，顺带卖葱，她给老詹骑了很多年的自行车，后来老詹过世这辆破旧的“飞利浦”牌自行车在老鲁的主持下留给了小赵。杨百顺从染坊跑出来后遇到老詹，没有路数加上老詹愿意提供工作，便答应老詹信了教，成了老詹的第九个信众，老詹给杨百顺取了摩西这个名字，希望杨百顺能和伟人摩西一样带世人走出困境，可惜摩西不是信教的料子，晚上听教义老是打瞌睡，老詹只好放弃发展摩西成为教徒。杨百顺虽然不信教了但是和老詹仍是好朋友，而镇上的人都知道了他叫摩西不知道他叫杨百顺，所以摩西的名字也就留下来了，此后就叫杨摩西了。摩西离了老詹介绍的竹叶社工作后又没了去处，干起了出力活成了挑水的摩西，挑水的摩西少有的好运在舞社火上高光了一次，被拉去跳阎罗赢得了县长的好评，进了县衙门成了种菜的摩西，种菜摩西后又入赘吴香香成了吴摩西。婚礼上昔日师傅老詹送来了一柄珍贵的银十字架，祝福吴摩西成婚也希望他不要忘了主，后十字架被吴香香让他的奸夫隔壁银铺老高融了打成耳坠（看到这里真的好气）。贾家庄的瞎老贾会弹三弦，老詹就好瞎老贾的这口三弦，最后也是因为去贾家庄传教路上淋了大雨，得了重感冒不过三天便去世了，老詹去世吴摩西作为好朋友出席了丧礼，在破庙里找到的老詹留下的教堂图纸以及背面的“恶魔的低语”，写到这里老詹的故事基本就结束了，吴摩西想要用编织的技术替老詹实现他那恢宏大气的教堂，也是在吴香香的一连串操作下不得不远走他乡。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;碎碎念的写了很多，都是凭记忆写了些小说的情节和脉络，没看过这本书的朋友想必是看得一头雾水，这里我真心推荐大家去读一读这本书，这里我也只读了上篇，有机会再继续读下篇。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><category>读书</category><author>Teclado</author></item></channel></rss>