<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Dingfan X.</title><description>技术 · 投研 · 工坊 · 旧思 · 阅读 —— 个人刊物</description><link>https://dingfanx.com/</link><item><title>UART DMA 首字节丢失与 USB 32 字节截断排查</title><link>https://dingfanx.com/tech/uart-dma-rx-overrun-and-usb-32byte-truncation/</link><guid isPermaLink="true">https://dingfanx.com/tech/uart-dma-rx-overrun-and-usb-32byte-truncation/</guid><pubDate>Sun, 31 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;排查 Cortex-M0 上 UART DMA 接收的两个问题。一是高波特率下首字节丢失——115200 下字节时间只有 87 μs，临界区稍长就会在 ISR 启用 DMA 之前 overrun，9600 下 1 ms 的余量则没问题；解法是让 UART 中断不被典型临界区屏蔽，或干脆用循环 DMA 让接收脱离 ISR 时序。二是低概率 32 字节截断——稳定的 32 这个数字指向 USB Bulk 端点的 &lt;code&gt;wMaxPacketSize&lt;/code&gt;，叠加 CH340 内部缓冲与缺 ZLP 的行为，相邻包之间的 UART 静默期偶尔会超过帧超时。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;现象&lt;/h2&gt;
&lt;p&gt;一个跑在 Cortex-M0 上的项目，UART 接收在 9600 波特率下表现正常，但切到 115200 后，有较高概率丢失首字节。&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;当前实现是第一个字节到达触发 UART 中断，中断里申请 buffer 并启动 DMA。也就是说，UART 中断必须在第二个字节到达之前完成 buffer 申请 + DMA 配置 + 使能，否则数据寄存器 RDR 会被覆盖（硬件上对应 Overrun Error）。如果首字节到达时主流程刚好处在关中断的临界区内，UART 中断被挂起，等临界区退出再处理时，首字节已经被后续字节冲掉了。&lt;/p&gt;
&lt;p&gt;为什么 9600 几乎不出问题，而 115200 频繁出问题？算一下就清楚。&lt;/p&gt;
&lt;p&gt;UART 按常见的 8-N-1 帧格式（1 起始位 + 8 数据位 + 1 停止位 = 10 bit/字节）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;9600 baud：10 / 9600 ≈ 1042 μs/byte&lt;/li&gt;
&lt;li&gt;115200 baud：10 / 115200 ≈ 86.8 μs/byte&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，从首字节进入 RDR 到下一字节到来，9600 下有大约 1 ms 的窗口，115200 下只有约 87 μs。典型嵌入式工程的临界区——Flash 操作、跨外设的多寄存器配置、嵌套保护——持续几十到上百微秒并不少见。87 μs 这个值正好踩在易出问题的边界上；1 ms 则给到了非常充裕的余量，绝大多数合理时长的临界区都容纳得下。&lt;/p&gt;
&lt;h2&gt;几种解决方向&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;方案一：分级临界区&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;提供两套临界区：传统的全关中断用于真正需要原子性的场景；另一套关除 UART 外的所有中断用于需要保留 UART 实时响应的场景。&lt;/p&gt;
&lt;p&gt;理想情况下这应该用优先级阈值屏蔽来实现——Cortex-M3 及以上有 &lt;a href=&quot;https://developer.arm.com/documentation/dui0552/a/the-cortex-m3-processor/programmers-model/core-registers&quot;&gt;BASEPRI&lt;/a&gt; 寄存器，可以&quot;只屏蔽优先级低于某阈值的中断&quot;，UART 中断放进 BASEPRI 不屏蔽的范围就好。但 Cortex-M0 实现的是 ARMv6-M，&lt;a href=&quot;https://developer.arm.com/documentation/dui0497/a/cortex-m0-peripherals/system-control-block/interrupt-control-and-state-register&quot;&gt;只有 PRIMASK&lt;/a&gt;，要么全开要么全关，没有原生的阈值机制，只能手动操作 NVIC 的 ISER/ICER 来做选择性 mask。嵌套临界区、ISR 中再次进出临界区、关中断期间 NVIC 状态的保存/恢复都需要小心，否则容易引入更隐蔽的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案二：循环 DMA + Idle Line 触发&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;UART 持续运行在 Circular DMA 模式，DMA 通道不停搬运，每 ms 轮询 buffer 来判断是否有新数据，进而及时将数据拷贝出去。&lt;/p&gt;
&lt;p&gt;这条路最干净，根本上规避了中断响应来不及的问题，因为 DMA 在数据搬运上完全不依赖 CPU。代价是 DMA 通道数。部分入门级 MCU 上 DMA 通道有限，多个通讯口子并存时不一定排得开。&lt;/p&gt;
&lt;h2&gt;一个未消除的低概率现象：32 字节截断&lt;/h2&gt;
&lt;p&gt;不论方案一二，进一步测试都发现一个极低概率现象：长帧偶尔会在第 32 字节处被截断。&lt;/p&gt;
&lt;p&gt;错误模式高度一致——总是 32 字节。如果是判帧机制本身的概率性失效（比如硬件计时器抖动、中断丢失），截断点应当随机分布。这么稳定的 32，必然对应着某个真实存在的 32 字节边界。&lt;/p&gt;
&lt;p&gt;回想起之前调试 USB 串口时遇到过的32 字节整倍数数据发不出去问题，这指向了 USB 协议的一个参数：&lt;code&gt;wMaxPacketSize&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;根据 &lt;a href=&quot;https://www.usb.org/document-library/usb-20-specification&quot;&gt;USB 2.0 Specification 第 5.8.3 节&lt;/a&gt;，Full-Speed Bulk 端点的 &lt;code&gt;wMaxPacketSize&lt;/code&gt; 只能是 8、16、32 或 64 之一（USB-Serial 转换器普遍走 Bulk 端点）。我使用的上位机连接的 USB-Serial 转换器协商出 32 字节。&lt;/p&gt;
&lt;p&gt;这里值得展开一下：USB-Serial 转换器有不同的芯片实现，常见的 FTDI（&lt;a href=&quot;https://ftdichip.com/&quot;&gt;Future Technology Devices International&lt;/a&gt;，一家苏格兰公司，FT232/FT2232 等 USB-Serial 芯片的老牌厂商）和 CP210x 协商出来通常是 64 字节，而 &lt;a href=&quot;https://www.wch-ic.com/products/CH340.html&quot;&gt;CH340/CH341&lt;/a&gt; 系列协商出来恰好是 32 字节。换言之，32 字节这个特征已经把芯片范围缩得相当窄。&lt;/p&gt;
&lt;h2&gt;可能的具体诱因&lt;/h2&gt;
&lt;p&gt;USB 是基于 1 ms 帧调度的协议。Full-Speed 上 SOF（Start of Frame）每 1 ms 发送一次，把总线时间切片。每个 &lt;code&gt;wMaxPacketSize&lt;/code&gt; 大小的数据需要等到主机发起一次 IN/OUT 事务才能传输。Bulk 传输优先级最低，遵循&quot;带宽可用时执行&quot;原则——理论上一帧可以塞下多个 Bulk 包（32 字节 payload 在 12 Mbps 上传输不过 ~21 μs），但实际能不能塞下取决于等时/中断传输的占用、主机控制器对 Bulk 队列的调度，以及主机端 USB 驱动到用户态的数据通路是否被及时调度。&lt;/p&gt;
&lt;p&gt;FTDI 的 &lt;a href=&quot;https://ftdichip.com/wp-content/uploads/2020/08/AN232B-04_DataLatencyFlow.pdf&quot;&gt;AN232B-04&lt;/a&gt; 应用笔记里明确指出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;USB does not transfer data using interrupts. It uses a scheduled system and as a result, there can be periods when the USB request does not get scheduled and, if handshaking is not used, data loss will occur.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;For a USB device, data transfer is done in packets. If data is to be sent from the PC, then a packet of data is built up by the device driver and sent to the USB scheduler. This scheduler puts the request onto the list of tasks for the USB host controller to perform. This will typically take at least 1 millisecond to execute because it will not pick up the new request until the next &apos;USB Frame&apos; (the frame period is 1 millisecond).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，在 OS 用户态进程被抢占、驱动线程让出 CPU、或 USB 主机控制器队列被高优先级设备占据的瞬间，确实存在 Bulk 传输被推迟数十毫秒的可能。&lt;/p&gt;
&lt;p&gt;把视角进一步收窄到具体芯片——CH340 系列在这件事上还有两个值得注意的硬件行为，都被 Linux 内核 &lt;code&gt;ch341.c&lt;/code&gt; 驱动源码及相关 commit 历史明确记载：&lt;/p&gt;
&lt;p&gt;第一，CH340A 在 OUT 方向（主机 → CH340 → UART）默认会在内部缓冲数据直到收满 32 字节才向 UART 转发，除非驱动主动设置某个寄存器的 bit 7 来关闭这个行为。源码注释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CH341A buffers data until a full endpoint-size packet (32 bytes) has been received unless bit 7 is set. (&lt;a href=&quot;https://github.com/torvalds/linux/blob/master/drivers/usb/serial/ch341.c&quot;&gt;linux/drivers/usb/serial/ch341.c&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第二，CH340 系列不会主动发 ZLP。Linux 内核 2021 年的一次 commit 把 ch341 driver 的 bulk-in 缓冲改回端点大小，理由是 &lt;a href=&quot;https://lkml.iu.edu/hypermail/linux/kernel/2108.3/00472.html&quot;&gt;&quot;These devices do not appear to send a zero-length packet when the transfer size is a multiple of the bulk-endpoint max-packet size.&quot;&lt;/a&gt; 而 USB 规范规定，Bulk 传输结束的标志之一就是出现一个长度小于 &lt;code&gt;wMaxPacketSize&lt;/code&gt; 的短包；如果一次传输恰好是 &lt;code&gt;wMaxPacketSize&lt;/code&gt; 的整数倍，发送端必须额外发一个 ZLP 来显式告知接收端&quot;传输结束&quot;，否则接收端会持续等待。这就是之前32 字节整倍数发不出去的根源（&lt;a href=&quot;https://argon.blue/blog/programming/2023/12/16/usb-zlp/&quot;&gt;Argon Blue 的这篇博客&lt;/a&gt;对 ZLP 在 CDC 中的角色描述得很清楚，虽然 CH340 不走 CDC，但 ZLP 机制是共通的）。&lt;/p&gt;
&lt;p&gt;把这几条线索叠起来，32 字节截断的更完整解释是：&lt;/p&gt;
&lt;p&gt;数据从上位机 App → USB Driver → CH340 → UART → 设备。芯片以 32 字节为最小调度单元在 USB 与 UART 间转发；正常情况下若干个 32 字节包紧密相连，UART 端看起来是连续的字节流。但当主机系统负载、驱动调度、或者 ZLP 行为缺失带来的等待，使得两个相邻 32 字节包之间的 UART 静默期超过设备侧的字符间帧超时，UART 接收端就判定当前帧结束，把已经收到的 32 字节抛给上层。多个因素叠加，恰好对应到 32 字节这个稳定的截断边界。&lt;/p&gt;
&lt;p&gt;需要承认，这条解释链不是单一因果，更像是一个具备这些条件就有概率出现的复合现象。低概率也正是 USB 调度抖动 + 硬件缓冲 + 主机负载的偶发交叠所致。&lt;/p&gt;
&lt;h2&gt;验证与解决方向&lt;/h2&gt;
&lt;p&gt;短期排查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;换一台 &lt;code&gt;wMaxPacketSize&lt;/code&gt; 非 32 字节的主机做对照，看截断频率是否消失&lt;/li&gt;
&lt;li&gt;增大字符间帧超时做对比，逼近真实调度抖动的上限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;长期解法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;协议层不要单纯依赖字符间超时来判帧。更稳健的做法是 长度前缀 + CRC，或者 SLIP 风格的 framing。这样即使 USB 端引入了几十毫秒抖动，也能从字节流正确恢复出帧边界&lt;/li&gt;
&lt;li&gt;如果产品形态允许，避免在传输链路上引入有已知缺陷的 USB-Serial 芯片&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.usb.org/document-library/usb-20-specification&quot;&gt;USB 2.0 Specification&lt;/a&gt;, Section 5.8.3 &lt;em&gt;Bulk Transfer Packet Size Constraints&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ftdichip.com/wp-content/uploads/2020/08/AN232B-04_DataLatencyFlow.pdf&quot;&gt;FTDI Application Note AN232B-04&lt;/a&gt;, &lt;em&gt;Data Throughput, Latency and Handshaking Processes between Hardware and a Software Driver&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.beyondlogic.org/usbnutshell/usb4.shtml&quot;&gt;USB in a NutShell — Chapter 4: Endpoint Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://argon.blue/blog/programming/2023/12/16/usb-zlp/&quot;&gt;Argon Blue: Much ado about nothing: USB zero-length packets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.arm.com/documentation/dui0497/a/&quot;&gt;ARM Cortex-M0 Devices Generic User Guide&lt;/a&gt;, 特别是 PRIMASK 与 NVIC 章节&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/torvalds/linux/blob/master/drivers/usb/serial/ch341.c&quot;&gt;linux/drivers/usb/serial/ch341.c&lt;/a&gt; 中关于 CH341A 32 字节缓冲行为的注释&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lkml.iu.edu/hypermail/linux/kernel/2108.3/00472.html&quot;&gt;&quot;USB: serial: ch341: fix character loss at high transfer rates&quot; 的 revert commit log&lt;/a&gt;，明确记录了 CH341 不发 ZLP 的行为&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>EEPROM 页回绕问题调试记录</title><link>https://dingfanx.com/tech/eeprom-page-wrap-around-debug/</link><guid isPermaLink="true">https://dingfanx.com/tech/eeprom-page-wrap-around-debug/</guid><pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;记录一下今天遇到的一个 bug，由于软件 eeprom 配置错误导致写回绕，数据丢失 + 数据覆盖，虽然定位到问题后解决比较简单，但搜集线索和排查耗费了较多时间。&lt;/p&gt;
&lt;/blockquote&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;ol&gt;
&lt;li&gt;问题只在带真实 EEPROM/Flash 的硬件上复现，在 mock 掉 EEPROM 和 Flash 的纯软件仿真环境里，下载流程完全正常。&lt;/li&gt;
&lt;li&gt;出问题的数据都集中在 EEPROM 地址的起始区域，几份数据的存储地址很近。&lt;/li&gt;
&lt;li&gt;问题点并不集中于传输，设备启动阶段也观察到异常，共同点是两者都依赖 EEPROM 里的同一片区域。&lt;/li&gt;
&lt;li&gt;触发点在传输，不传输则遇不到，比如正常启动时不会遇到该问题。&lt;/li&gt;
&lt;li&gt;仅一款设备存在问题，其它型号正常。&lt;/li&gt;
&lt;li&gt;连续操作下，第一次下载前数据正常；第一次下载失败后读出&quot;异常 A&quot;；第二次下载失败后读出&quot;异常 B&quot;。异常本身有确定的模式，不是随机扰动。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;推测&lt;/h2&gt;
&lt;p&gt;基于上述几条线索，基本可以确定&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码逻辑无问题：因为 mock 的 eeprom 和 flash 并不存在问题。而且若程序有问题，如何解释问题不集中在一处呢？&lt;/li&gt;
&lt;li&gt;问题与 eeprom 有关，但与 eeprom 硬件故障无关：异常点处数据集中，异常模式稳定，若 eeprom 硬件故障，则不会保持稳定的异常模式。&lt;/li&gt;
&lt;li&gt;问题与数据传输时，与 eeprom 有关的数据项具有强关联。因为触发点很确定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;验证&lt;/h2&gt;
&lt;p&gt;事后回顾时，发现当时解决问题时的思路不够清晰，但是也不算糊涂。&lt;/p&gt;
&lt;p&gt;先去确认了当前工程的 eeprom 配置，发现工程配置的芯片型号是 XXXX，查阅的对应的 datasheet：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Page Write&lt;/strong&gt;
The Page Write mode allows up to 256 bytes to be written in a single Write cycle, provided that they are all located in the same page in the memory: that is, the most significant memory address bits, b16–b8, are the same. If more bytes are sent than will fit up to the end of the page, a &quot;roll-over&quot; occurs, i.e. the bytes exceeding the page end are written on the same page, from location 0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;发现它的 page size 大小是 256 bytes，于是请硬件同事确认该 eeprom 容量是否真的是 1Mbit（对应 256 bytes 的 page size），发现并不是，而是 500kbit，那么实际的 page size 就应该是 128 bytes。&lt;/p&gt;
&lt;p&gt;同时 datasheet 中提到了一个重要的机制 “roll-over”。但是在事后分析前，先简单将配置调整为 128 bytes 的 page size，尝试复现问题来确认解决是否有效，果然发现问题已经消失。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;当工程的 page size 配置为 256 bytes，实际物理 eeprom 的 page size 为 128 bytes 时，就会导致当写入的数据在 256 bytes 以内，但又恰好超过了 eeprom 中以 128 bytes 为倍数的物理边界时，就会导致驱动程序认为当前写入安全，未超出 256 bytes 的上界，不需要拆包直接写，但是实际硬件并非如此，没有被拆包的就会发生上述 “roll-over” 现象。导致 eeprom 前面已经写入的数据被覆盖掉，进而设备业务逻辑出现异常。&lt;/p&gt;
&lt;p&gt;但是这是事后分析猜得出的，实际上确实是思路不够清晰。本应该首先检查传输阶段哪些数据会向 eeprom 中写入，写入地址是多少，写入字节数是多少，是否会撞到特殊点处，比如 2 的幂次倍数据，数据是否跨页等。但是误打误撞从配置角度切入，发现了问题症结，然后从现象反推原因。&lt;/p&gt;
&lt;p&gt;后来为了确认，再次复现复盘了一下，发现实际写入过程与推理一致，传输过程中有几项需要写入 eeprom 的数据比较大块（130 bytes），恰好就踩到了 eeprom 128 bytes 的物理边界上，且由于写入地址非常接近 128 bytes 边界，所以回绕后的数据快追上数据头了，也就导致不仅自身传输业务受到了影响，在写入地址前的所有业务都受到了影响，但比较容易观测的只有设备启动时的异常。&lt;/p&gt;
&lt;h2&gt;修复&lt;/h2&gt;
&lt;p&gt;除了修改掉软件配置外，同时新增两个防御性措施：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Mock 行为对齐硬件&lt;/strong&gt;：mock 的实现也按真实的 page size 模拟 wrap-around 行为，否则会导致 mock 反而掩盖了问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在 mock 程序上新增几组 unit test&lt;/strong&gt;：跨页写入、起始地址恰好在 page 末尾、单次写满整页等。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;越是接近答案，越要保持头脑清晰&lt;/li&gt;
&lt;li&gt;Mock 须以复现硬件的关键约束为目标，包括 page size、写入耗时、边界行为等。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Chunking Strategies for RAG</title><link>https://dingfanx.com/tech/chunking-strategies-for-rag/</link><guid isPermaLink="true">https://dingfanx.com/tech/chunking-strategies-for-rag/</guid><description>基于 Pinecone Chunking Strategies 的精读笔记。重点整理了 chunk size 的双重影响、embedding 容量瓶颈与 lost-in-the-middle 的本质区别、RAG vs long context 的工程权衡、以及切分与增强的正交矩阵思维。</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文基于 Pinecone 文章和与 Claude 的讨论整理。后续会持续补充阅读其他文献（尤其是 Liu et al. 2023 等原始论文）后的修正与扩展。目前文中 Claude 提供的数据未经验证，无法保证可靠，但大方向的正确与否是很显然的，比如几种 chunk method 的效率和成本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. Chunking 的本质：信息完整度 vs 信息纯度的权衡&lt;/h2&gt;
&lt;p&gt;文章开头那句被反复引用的话：finding chunks that are big enough to contain meaningful information, while small enough to enable performant applications and low latency responses.&lt;/p&gt;
&lt;p&gt;简而言之：&lt;strong&gt;chunk size 是在&quot;信息完整度&quot;和&quot;信息纯度&quot;之间做权衡&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;太小 → 损失完整度。每块讲不清一件事，单独看缺乏必要的上下文。&lt;/li&gt;
&lt;li&gt;太大 → 损失纯度。每块讲了太多事，关键信号被稀释。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个 framing 比”找一个合适的大小“更有指导意义——它说明 chunk size 不是一个绝对最优值，而是与语料、query 分布、模型能力共同决定的一个平衡点。&lt;/p&gt;
&lt;h2&gt;2. chunk size 的双重影响：检索 vs 生成&lt;/h2&gt;
&lt;p&gt;chunk size 在 RAG pipeline 的两个阶段产生不同的影响，需要分别考虑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;检索阶段&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;chunk 太小 → 单个 chunk 信息缺乏上下文（比如没有主语的孤儿 chunk，it / this / above 等回指词），embedding 字面信息不足，检索时召不回。&lt;/li&gt;
&lt;li&gt;chunk 太大 → 多个话题被压缩成一个向量，每个话题的信号都被稀释，精确 query 难以匹配上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;生成阶段&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;chunk 太小 → 单个 chunk 信息不足，需要更多 chunk 才能拼出完整答案，挤占 LLM 的 context 预算。&lt;/li&gt;
&lt;li&gt;chunk 太大 → 输入 token 数增加，推理延迟上升、成本上升，且触发 lost-in-the-middle。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个双重影响解释了为什么 chunk size 没有简单公式可衡量的根本原因。因此优化应该面向两个阶段同时存在的多个失败模式来进行。&lt;/p&gt;
&lt;h2&gt;3. 被混淆的损失机制&lt;/h2&gt;
&lt;p&gt;读这篇文章时踩过的坑：&lt;strong&gt;把 embedding 容量瓶颈和 lost-in-the-middle 混为一谈&lt;/strong&gt;。虽然它们都是长文本上的信息丢失”，但发生在不同阶段、由不同机制造成。&lt;/p&gt;
&lt;h3&gt;机制 A：Embedding 的容量压缩损失&lt;/h3&gt;
&lt;p&gt;发生在检索阶段。embedding 模型把任意长度的文本压缩成一个固定维度的向量（比如 1024 维）。这是极度有损的压缩——输入越长，单位 token 能”占用“的向量空间越少。&lt;/p&gt;
&lt;p&gt;类比：embedding 维度像一张固定分辨率的画布。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1024 维 ≈ 1024 像素的画布&lt;/li&gt;
&lt;li&gt;一个句子 → 一张画布画一个苹果，细节清晰&lt;/li&gt;
&lt;li&gt;整本书 → 同样大小的画布画整个超市，全糊成一片&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际后果：把整个参考文档编码成单个向量，向量只能粗粒度地代表“这是一份某领域的文档”。当用户 query 是某个具体细节问题时，embedding 距离对不上，细节信息已经在压缩中丢失了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这就是为什么 chunking 不只为适配 context window，还要充分考虑 embedding 模型的有损压缩，以保留更多细节&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;机制 B：Lost-in-the-Middle&lt;/h3&gt;
&lt;p&gt;发生在生成阶段。Liu et al. (2023) 的实验显示：把回答问题所需的关键信息放在长 context 的不同位置，准确率呈 U 型曲线——开头和结尾最高，中间最低。这是 LLM 注意力机制和训练数据分布共同导致的结构性问题。&lt;/p&gt;
&lt;p&gt;需要特别注意“中间”指的是 &lt;strong&gt;LLM prompt 中物理位置的中间&lt;/strong&gt;，而不是&quot;被压缩进 embedding 时丢失的中间信息&quot;。这两个&quot;中间&quot;完全是不同的概念。&lt;/p&gt;
&lt;h3&gt;两个机制的对照&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Embedding 压缩损失&lt;/th&gt;
&lt;th&gt;Lost-in-the-Middle&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;生成阶段(LLM 推理时)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;涉及模型&lt;/td&gt;
&lt;td&gt;embedding 模型&lt;/td&gt;
&lt;td&gt;生成式 LLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;损失原因&lt;/td&gt;
&lt;td&gt;固定维度向量容量有限&lt;/td&gt;
&lt;td&gt;注意力机制 + 训练分布偏置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“中间”含义&lt;/td&gt;
&lt;td&gt;没有&quot;中间&quot;概念，是整体压缩&lt;/td&gt;
&lt;td&gt;LLM prompt 中物理位置的中间&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;4. RAG vs Long Context：为什么前者还没被淘汰&lt;/h2&gt;
&lt;p&gt;目前 LLM 的 context 已经越来越大，DeepSeek v4 甚至具备 1M 的上下文空间，那么 RAG 是否依然需要？&lt;/p&gt;
&lt;p&gt;答案是肯定的。&lt;/p&gt;
&lt;p&gt;最开始我的理解是，如果没有 RAG，那么 LLM 应该要自己去从整个输入中寻找需要的信息，但是 Claude 对我的理解进行了纠正：&lt;strong&gt;LLM 不在 context 里&quot;搜索&quot;信息&lt;/strong&gt;。它的工作方式是每生成一个 output token 都对整个 context 做一次 attention 计算。所谓塞 200k 进去让模型自己找，实际上是让模型为每个生成步骤都消化一遍 200k token。因此基于这个认识，回答可以从如下几个方面展开：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quality&lt;/strong&gt;：lost-in-the-middle 导致中间内容被忽略,回答质量反而下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Latency&lt;/strong&gt;：Self-attention 复杂度是 O(n²)。100k tokens 比 5k tokens 在 prefill 阶段不是慢 20 倍，而是慢约 400 倍(理论上限)。直接体现为 TTFT(Time To First Token)显著上升。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;：input token 按量计费。每次 query 处理整份文档是巨大浪费——RAG 的本质是把全文档 attention换成top-k chunk attention，n 从 200k 降到 2k，计算和计费都降两个数量级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;架构清晰性&lt;/strong&gt;：RAG 把检索和生成职责分离，可以独立优化、独立替换、独立评估。这是软件设计层面的清晰性，仅有 long context 的方案做不到。&lt;/p&gt;
&lt;p&gt;那如果将 prompt caching 机制考虑进来呢？答案是 &lt;strong&gt;prompt caching&lt;/strong&gt; 虽然可以缓存重复使用的 prompt 前缀，缓解部分 cost 问题。但它不能解决 quality 和 lost-in-the-middle 问题，因此总体上 RAG 仍然具备优势。&lt;/p&gt;
&lt;h3&gt;关于 prefill 和 decoding&lt;/h3&gt;
&lt;p&gt;LLM 推理延迟实际由两段组成:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prefill 阶段&lt;/strong&gt;：处理整个输入 prompt，这一步是 O(n²)，决定 TTFT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoding 阶段&lt;/strong&gt;：逐个生成 output token，借助 KV Cache 每个新 token 是 O(n)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KV Cache 解决的是 decoding 阶段的重复计算，但 prefill 阶段躲不开 O(n²)。所以“长 context 慢”的痛点主要在 prefill。这也是 prompt caching 能缓解 cost 的原因——它本质上是缓存 prefill 的中间结果(K/V)。&lt;/p&gt;
&lt;h2&gt;5. 几种 Chunking 方法&lt;/h2&gt;
&lt;p&gt;文章里的方法分类一开始读起来比较散，整理成层级会清楚得多:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Fixed-size chunking(不看内容,纯按 token 数切)
Content-aware chunking(看内容决定切分)
├── Sentence / paragraph splitting(NLTK, spaCy)
├── Recursive Character Splitter(LangChain)
├── Document structure-based(Markdown / HTML / LaTeX / PDF)
└── Semantic Chunking(Kamradt)
Contextual Chunking with LLMs(独立维度)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;strong&gt;Contextual Chunking 不在切分方法的层级里&lt;/strong&gt;——它是对已切好的 chunk 做上下文增强，正交于切分方法。这是下一节要讨论的关键。&lt;/p&gt;
&lt;h2&gt;6. 切分与增强的正交性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Semantic Chunking ≠ Contextual Chunking&lt;/strong&gt;。虽然两者都涉及 LLM 或 embedding，名称相似，但解决的问题完全不同。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Semantic Chunking&lt;/th&gt;
&lt;th&gt;Contextual Chunking&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;利用 LLM/embedding 做什么&lt;/td&gt;
&lt;td&gt;用 embedding 距离检测话题切换&lt;/td&gt;
&lt;td&gt;用 LLM 给每个 chunk 生成上下文摘要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出&lt;/td&gt;
&lt;td&gt;不同大小的 chunk&lt;/td&gt;
&lt;td&gt;增强后的 chunk（原文 + 摘要）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否互斥&lt;/td&gt;
&lt;td&gt;否，可以组合使用&lt;/td&gt;
&lt;td&gt;否，可以组合使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;把它们拆开来看，一个 RAG 系统的 chunking 设计实际上是两个独立维度的组合:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;切分维度(必选其一):    fixed-size | recursive | doc-structure | semantic
增强维度(可选叠加):    none | heading prefix | contextual chunking | ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种&lt;strong&gt;正交分离的思维&lt;/strong&gt;意味着可以独立调试每个维度——先选定切分方法，再决定要不要叠加增强；或者先验证基础切分够不够好，再考虑是否需要 contextual 增强。&lt;/p&gt;
&lt;h2&gt;7. Recursive 的本质:用形式逼近语义&lt;/h2&gt;
&lt;p&gt;Recursive Character Splitter 的默认分隔符列表是 &lt;code&gt;[&quot;\n\n&quot;, &quot;\n&quot;, &quot; &quot;, &quot;&quot;]&lt;/code&gt;，按优先级递归切分。它通常比 fixed-size 更”聪明“，因为它利用的不是语义信号本身，而是&lt;strong&gt;人类书写习惯的代理信号(proxy signal)&lt;/strong&gt;。人写作时会用 &lt;code&gt;\n\n&lt;/code&gt; 表示话题切换、&lt;code&gt;\n&lt;/code&gt; 表示句子结尾——这些都是结构形式，不是语义。Recursive 的核心假设是”形式边界 ≈ 语义边界“。&lt;/p&gt;
&lt;p&gt;这个假设在格式良好的文档上成立，但形式边界并非永久成立，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一段落里也可能切换话题(&lt;code&gt;\n\n&lt;/code&gt; 失灵)&lt;/li&gt;
&lt;li&gt;一个话题可能跨多段(&lt;code&gt;\n\n&lt;/code&gt; 过度切分)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;理解了 Recursive 是 proxy signal 之后，就知道它什么时候会失灵——结构形式与实际话题边界不一致的文档（语音转写、聊天记录、不规范笔记）。这种文档恰好是 Semantic Chunking 的舞台。&lt;/p&gt;
&lt;h2&gt;8. Semantic Chunking 的真实成本&lt;/h2&gt;
&lt;p&gt;很多教程会推荐 Semantic Chunking 作为&quot;更聪明&quot;的选择，但生产环境很少用它。原因是成本：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Recursive&lt;/th&gt;
&lt;th&gt;Semantic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;计算成本&lt;/td&gt;
&lt;td&gt;接近 0，纯字符串处理&lt;/td&gt;
&lt;td&gt;每个句子都要 embedding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;处理时间&lt;/td&gt;
&lt;td&gt;1MB 文档 &amp;lt; 1 秒&lt;/td&gt;
&lt;td&gt;1MB 文档约几分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 调用&lt;/td&gt;
&lt;td&gt;0&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;需处理 batch、阈值调参&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Pinecone 用 &quot;experimental&quot; 形容它不是说效果差——而是性价比还没到能作为默认选项的程度。处理 100k 文档的语料库时，semantic 切分一次可能要跑几小时，这对快速迭代是致命的。&lt;/p&gt;
&lt;p&gt;生产系统里更常见的组合是 &lt;strong&gt;&quot;Recursive 或 structure-based 切分 + contextual chunking 增强&quot;&lt;/strong&gt;——避开 semantic 的预处理代价,但通过后处理弥补语义自洽性。这又是正交分离思维的应用。&lt;/p&gt;
&lt;h2&gt;9. 工程选型的多维权衡&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;Quality&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-size&lt;/td&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;Recursive&lt;/td&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;Doc-structure&lt;/td&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;Semantic&lt;/td&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;+ Contextual 增强&lt;/td&gt;
&lt;td&gt;提升&lt;/td&gt;
&lt;td&gt;略慢&lt;/td&gt;
&lt;td&gt;略高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;工程选型不是”选最好的“，而是&lt;strong&gt;在 quality / speed / cost 三角形里找项目能接受的点&lt;/strong&gt;。语音转写场景可以接受 semantic 的高成本，因为没别的选择；规范文档场景就完全没必要付 semantic 的代价。&lt;/p&gt;
&lt;p&gt;数据规模也是一个隐含约束:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小数据集(&amp;lt; 1k 文档)：怎么选都行,成本差异微不足道&lt;/li&gt;
&lt;li&gt;中等数据集(1k - 100k 文档)：开始要权衡，semantic 的预处理时间显著&lt;/li&gt;
&lt;li&gt;大数据集(&amp;gt; 100k 文档)：semantic 几乎不可行，必须用更便宜的方法 + 后处理增强&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;10. 把 chunk size 当超参数搜索&lt;/h2&gt;
&lt;p&gt;文章最后给出的调优指南本质上是一个超参数搜索：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With a representative dataset, create the embeddings for the chunk sizes you want to test and save them in your index (or indices).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;把它翻译成操作步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构建 query 集&lt;/strong&gt;：从真实使用场景采样问题，人工标注每个 query 应该命中的 gold chunk&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dev / test split&lt;/strong&gt;:按 7:3 分，test set 锁起来不看&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;候选 size 列表&lt;/strong&gt;：[128, 256, 512, 1024]&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个 size 切分 → embed → 建索引&lt;/strong&gt;：用不同 namespace 隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在 dev set 上跑 query&lt;/strong&gt;：评估 recall@k、MRR 等指标&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选出 dev set 上最优 size&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在 test set 上验证一次&lt;/strong&gt;：报告最终性能;如果与 dev 差距大说明 dev 上 overfit&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;起初向 Claude 阐述我的想法时，说的是训练集和测试集，后来它纠正了我：RAG 评估的标准术语是 &lt;strong&gt;dev set / test set&lt;/strong&gt;,不是”训练集/测试集“——因为 RAG 系统里没有”模型训练“这一步，调的是 pipeline 工程参数,不是模型权重。&lt;/p&gt;
&lt;p&gt;结合之前的量化探索，考虑进阶一步：&lt;strong&gt;Monte Carlo 重采样验证稳定性&lt;/strong&gt;。多次随机重采样 dev/test split，看选出的“最优 chunk size”在不同 split 下是否稳定。如果 256 在 split 1 是最优、512 在 split 2 是最优，说明信号很弱，需要更大的 query 集才能做出可靠决策。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pinecone.io/learn/chunking-strategies/&quot;&gt;Pinecone: Chunking Strategies for LLM Applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2307.03172&quot;&gt;Liu et al., &quot;Lost in the Middle: How Language Models Use Long Contexts&quot; (2023)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/tutorials/LevelsOfTextSplitting/5_Levels_Of_Text_Splitting.ipynb&quot;&gt;Greg Kamradt&apos;s notebook on 5 Levels of Text Splitting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/news/contextual-retrieval&quot;&gt;Anthropic: Contextual Retrieval&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Hello, World</title><link>https://dingfanx.com/tech/hello-world/</link><guid isPermaLink="true">https://dingfanx.com/tech/hello-world/</guid><description>博客的第一篇文章。</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是博客上线后的第一篇文章,只是用来验证流程。&lt;/p&gt;
&lt;p&gt;后面会陆续把已经写好的笔记搬过来,从 chunking 学习笔记开始。&lt;/p&gt;
</content:encoded></item><item><title>note-agent Tag System Design</title><link>https://dingfanx.com/tech/note-agent-tag-system-design/</link><guid isPermaLink="true">https://dingfanx.com/tech/note-agent-tag-system-design/</guid><description>讨论 note-agent 设计过程中 tag 设定的最佳实践，目标是降低 LLM 调用开销、提升 eval 可靠性、并减少阅读时的认知成本。</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;note-agent 在处理部分语料时，存在 tag 推荐过于宽泛的现象（例如对一篇关于豆包的新闻只输出 &lt;code&gt;AI&lt;/code&gt; 和 &lt;code&gt;付费&lt;/code&gt;）。在与 Claude 讨论时进一步发现：即便换上更精准的 tag，多个 tag 之间也存在「维度混杂」的扁平化问题——主体、概念、项目归属被塞进同一个命名空间。两个问题的本质相通，于是写下这篇关于 tag 设定的标准与最佳实践。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. tag 与 folder 的正交性&lt;/h2&gt;
&lt;h3&gt;原则&lt;/h3&gt;
&lt;p&gt;类似于 chunking design，&lt;strong&gt;能由文件结构本身提供的信息，就不必再让 LLM 通过语义来生成&lt;/strong&gt;——这是工程上“用确定性换概率性”的常规手段。一篇笔记的 folder 路径已经具备了大方向的前置信息，如：当一级目录是 &lt;code&gt;AI/&lt;/code&gt; 时，所有归入此目录的笔记天然具有&quot;AI 类&quot;属性，再用 &lt;code&gt;AI&lt;/code&gt; 作 tag 是&lt;strong&gt;信息冗余&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此 tag 设计的第一条规范是：&lt;strong&gt;tag 与 folder 提供的信息应当非冗余&lt;/strong&gt;——禁止 tag 与目录名重复。&lt;/p&gt;
&lt;h3&gt;关于&quot;正交&quot;一词的语义说明&lt;/h3&gt;
&lt;p&gt;此处的&quot;正交&quot;采用 &lt;em&gt;The Pragmatic Programmer&lt;/em&gt; 第 10 章给出的工程语义[2]：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Two or more things are orthogonal if changes in one do not affect any of the others.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;即&lt;strong&gt;独立、解耦、信息不重复编码&lt;/strong&gt;——比数学上严格的&quot;内积为零、互信息为零&quot;要弱。Wikipedia 的 &lt;em&gt;Orthogonality (programming)&lt;/em&gt; 词条也沿用此约定[3]。tag 与 folder 在严格数学意义上&lt;strong&gt;不可能完全独立&lt;/strong&gt;（folder 路径会限制 tag 的合理取值），但可以做到工程意义上的非冗余：改 folder 不必同步改 tag，反之亦然。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. tag 命名的 schema&lt;/h2&gt;
&lt;p&gt;衡量一个 agent 的可靠性需要充分的 eval 支撑。为了让 eval 能稳定打分，应当&lt;strong&gt;优先建立机械可验证的命名规则&lt;/strong&gt;——能用正则解决的事情就不要消耗 LLM token：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;专有名词&lt;/strong&gt;：保留官方英文写法或 PascalCase（如 &lt;code&gt;ByteDance&lt;/code&gt;、&lt;code&gt;OpenAI&lt;/code&gt;、&lt;code&gt;iPhone&lt;/code&gt;、&lt;code&gt;GPT-4&lt;/code&gt;、&lt;code&gt;LLaMA&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;概念类 tag&lt;/strong&gt;：全小写 + kebab-case（&lt;code&gt;ai-monetization&lt;/code&gt;、&lt;code&gt;ai-chatbot&lt;/code&gt;、&lt;code&gt;ad-driven-growth&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;禁止&lt;/strong&gt;：空格、下划线混用、中文（与代码标识符的英文规范保持一致）、与目录名重复的词&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;确立后将这些规则作为硬约束写入 system prompt，并在 eval 框架中加入正则校验层，不符合命名规范的 tag 直接扣分。这样就把 &lt;strong&gt;tag 质量&lt;/strong&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;regex（机械、确定）&lt;/td&gt;
&lt;td&gt;接近零&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;语义准确性&lt;/td&gt;
&lt;td&gt;LLM judge&lt;/td&gt;
&lt;td&gt;消耗 token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;前者免费且确定，后者才需要消耗推理成本——这是又一条工程上&quot;非冗余分解&quot;的应用。&lt;/p&gt;
&lt;h3&gt;关于 LLM judge 的可靠性&lt;/h3&gt;
&lt;p&gt;一个常见的疑虑是：tag 本身就是 LLM 生成的，再让 LLM 来 judge 不是循环吗？&lt;/p&gt;
&lt;p&gt;并不是。原因有三：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;生成与判别不对称&lt;/strong&gt;：生成 tag 时模型要平衡多个目标（具体性、规范性、与 folder 不冗余等），判别时只需回答单一窄问题，注意力集中，错误率远低于生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实证支持&lt;/strong&gt;：Zheng et al. (2023) 的研究表明，强 LLM judge（如 GPT-4）与人类专家的一致率可达 80% 以上，&lt;strong&gt;与人类专家之间的互相一致率持平&lt;/strong&gt;[4][5]。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓解偏差&lt;/strong&gt;：让 judge 模型与生成器&lt;strong&gt;不同&lt;/strong&gt;（例如用 DeepSeek 生成、用 Claude judge），可直接规避 self-enhancement bias[4]。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但 LLM-as-judge 不是无脑可信的，需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拆解为多个&lt;strong&gt;单维度 yes/no 判别&lt;/strong&gt;而非整体打分&lt;/li&gt;
&lt;li&gt;用 30-50 条&lt;strong&gt;人工标注样本&lt;/strong&gt;做小规模校准，确认 judge 与人工的一致率 ≥ 75% 后再大规模部署&lt;/li&gt;
&lt;li&gt;警惕 position bias、verbosity bias、self-enhancement bias 等已知偏差[4]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一步在思路上等同于量化策略的 out-of-sample 验证：先在小样本上证明评估器自身可靠，再用它去评估大规模产出。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. tag 命名的 namespace&lt;/h2&gt;
&lt;h3&gt;问题：扁平 namespace 的维度混淆&lt;/h3&gt;
&lt;p&gt;目前个人笔记中的 tag 存在维度混淆，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Doubao&lt;/code&gt; —— &lt;strong&gt;主体&lt;/strong&gt;（这篇笔记关于谁/是什么）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ai-monetization&lt;/code&gt; —— &lt;strong&gt;概念&lt;/strong&gt;（涉及什么思想/范畴）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;note-agent&lt;/code&gt; —— &lt;strong&gt;项目归属&lt;/strong&gt;（属于哪个工程）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wechat&lt;/code&gt; —— &lt;strong&gt;来源&lt;/strong&gt;（笔记从哪里导入，例如经由微信渠道同步）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这四类 tag 处于同一个扁平的 namespace 时会带来一个隐蔽问题：当未来搜索 &lt;code&gt;note-agent&lt;/code&gt;，预期返回的是该项目的设计文档，还是为这个项目搜集的语料？两种意图会冲突，扁平 tag 不会告诉用户它们的区别。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这本质上是上一节&quot;正交性&quot;的同一原则反过来用——不同维度的信息不应当挤进同一个命名空间&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;解决方案：用 &lt;code&gt;/&lt;/code&gt; 显式声明维度&lt;/h3&gt;
&lt;p&gt;Obsidian 原生支持嵌套 tag，使用 &lt;code&gt;/&lt;/code&gt; 作为分层符号[6][7]。建议规划如下 namespace：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Namespace&lt;/th&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;code&gt;project/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;项目归属&lt;/td&gt;
&lt;td&gt;kebab-case（匹配 repo 名）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;project/note-agent&lt;/code&gt;, &lt;code&gt;project/dlms-agent&lt;/code&gt;, &lt;code&gt;project/stock-system&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topic/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;概念 / 方法论&lt;/td&gt;
&lt;td&gt;kebab-case&lt;/td&gt;
&lt;td&gt;&lt;code&gt;topic/orthogonality&lt;/code&gt;, &lt;code&gt;topic/eval-design&lt;/code&gt;, &lt;code&gt;topic/prompt-engineering&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;真实世界主体（人 / 公司 / 产品）&lt;/td&gt;
&lt;td&gt;保留原始大小写&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subject/Doubao&lt;/code&gt;, &lt;code&gt;subject/ByteDance&lt;/code&gt;, &lt;code&gt;subject/OpenAI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;笔记类型&lt;/td&gt;
&lt;td&gt;kebab-case&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type/news-summary&lt;/code&gt;, &lt;code&gt;type/design-doc&lt;/code&gt;, &lt;code&gt;type/retrospective&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;笔记的导入来源&lt;/td&gt;
&lt;td&gt;kebab-case&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source/wechat&lt;/code&gt;, &lt;code&gt;source/rss&lt;/code&gt;, &lt;code&gt;source/manual&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;渐进原则：schema 应被使用反推，而非先验设计&lt;/h3&gt;
&lt;p&gt;不要一开始就启用全部 namespace。建议先用 &lt;code&gt;project/&lt;/code&gt; 加无前缀的混合 tag（subject、topic 暂时混在一起）跑两到四周，观察实际检索行为缺什么——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;我想看所有 note-agent 的设计文档&quot; → 需要 &lt;code&gt;type/design-doc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&quot;我想看所有提到正交性的笔记&quot; → 需要 &lt;code&gt;topic/orthogonality&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&quot;我想看所有从微信同步过来的内容&quot; → 需要 &lt;code&gt;source/wechat&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;让 pain point 来驱动 namespace 的扩张。这与量化系统中&quot;先有信号再加约束&quot;的开发节奏是一致的：&lt;strong&gt;结构源于使用，而不是反过来&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 从设计到落地：本站的标签体系实现&lt;/h2&gt;
&lt;p&gt;上面三节确立了原则，但&quot;写在文档里的规范&quot;会随时间漂移。真正让它生效的，是把规则&lt;strong&gt;代码化为唯一事实源&lt;/strong&gt;，并在构建期强制执行——不合规就直接 fail build。本站（Astro）的落地方式如下，所有规则集中在 &lt;code&gt;src/lib/tags.ts&lt;/code&gt; 一处。&lt;/p&gt;
&lt;h3&gt;4.1 命名空间白名单&lt;/h3&gt;
&lt;p&gt;在第 3 节草案的基础上补齐了 &lt;code&gt;series/&lt;/code&gt;，最终启用六个命名空间：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Namespace&lt;/th&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;code&gt;subject/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章关于的核心对象（人 / 公司 / 产品 / 技术实体）&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;原始大小写 / PascalCase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topic/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;概念 / 方法论 / 议题&lt;/td&gt;
&lt;td&gt;不限&lt;/td&gt;
&lt;td&gt;kebab-case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章体裁（封闭枚举）&lt;/td&gt;
&lt;td&gt;1（惯例）&lt;/td&gt;
&lt;td&gt;见 4.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;project/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所属工程项目，匹配 repo 名&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;kebab-case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;series/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连载系列，把多篇文章串成一条阅读路径&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;kebab-case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;内容的导入来源 / 渠道&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;kebab-case&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;subject/&lt;/code&gt; 限两个，是为了逼出&quot;这篇到底关于谁&quot;的取舍——主体一多，检索时的指向性就被稀释（呼应第 3 节的维度混淆问题）。&lt;/p&gt;
&lt;h3&gt;4.2 type 作为封闭枚举&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;type/&lt;/code&gt; 的取值不是自由文本，而是一个受控集合：&lt;code&gt;retrospective&lt;/code&gt; / &lt;code&gt;case-study&lt;/code&gt; / &lt;code&gt;methodology&lt;/code&gt; / &lt;code&gt;study-note&lt;/code&gt; / &lt;code&gt;design-doc&lt;/code&gt; / &lt;code&gt;news-summary&lt;/code&gt; / &lt;code&gt;meta&lt;/code&gt; / &lt;code&gt;note&lt;/code&gt;。新增体裁必须先改枚举、再使用——避免&quot;随手造一个新 type&quot;造成同义碎片。&lt;/p&gt;
&lt;h3&gt;4.3 受控词表与同义词&lt;/h3&gt;
&lt;p&gt;历史写法与同义词集中登记，构建期会拒绝非规范写法并指向规范标签（如 &lt;code&gt;rag → subject/RAG&lt;/code&gt;、&lt;code&gt;learning-note → type/study-note&lt;/code&gt;）。这等价于 Stack Overflow 的 tag synonyms[8]：永远只让一个规范标签存在，别让 &lt;code&gt;llm&lt;/code&gt; 与 &lt;code&gt;LLM&lt;/code&gt; 各自成页。&lt;/p&gt;
&lt;h3&gt;4.4 三道关口，一个事实源&lt;/h3&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;zod &lt;code&gt;superRefine&lt;/code&gt;（内容 schema）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;astro build&lt;/code&gt; / &lt;code&gt;astro check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不合规直接 fail build，并指出文件与原因&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm run lint:tags&lt;/code&gt;（vitest）&lt;/td&gt;
&lt;td&gt;本地 / CI&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;按命名空间分组、显示标签说明，复用同一份白名单&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;命名合规性由正则（机械、零成本）守住，语义准确性才留给人工 / LLM judge——正是第 2 节&quot;非冗余分解&quot;的直接落地：能用确定性手段解决的，就不消耗推理成本。&lt;/p&gt;
&lt;h3&gt;4.5 标签说明（tag-wiki）&lt;/h3&gt;
&lt;p&gt;每个标签页顶部可显示一句定义（来自 &lt;code&gt;TAG_DESCRIPTIONS&lt;/code&gt;），借鉴 Stack Overflow 的 tag-wiki：让标签不只是过滤器，也是一个有解释的索引入口。&lt;/p&gt;
&lt;h3&gt;4.6 仍然遵循&quot;结构源于使用&quot;&lt;/h3&gt;
&lt;p&gt;以上数量上限与枚举都是&lt;strong&gt;起点而非教条&lt;/strong&gt;，且全部集中在一处可改。第 3 节的渐进原则依然成立：让真实的检索 pain point 驱动枚举与命名空间的演化，而不是先验地一次配全。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;[1] Denvir, B. T. (1979). On orthogonality in programming languages. &lt;em&gt;ACM SIGPLAN Notices&lt;/em&gt;, 14(7), 18–30. https://dl.acm.org/doi/10.1145/953029.808475&lt;/p&gt;
&lt;p&gt;[2] Hunt, A., &amp;amp; Thomas, D. (2019). Topic 10: Orthogonality. In &lt;em&gt;The Pragmatic Programmer: Your Journey to Mastery&lt;/em&gt; (20th Anniversary Edition). Addison-Wesley. https://www.oreilly.com/library/view/the-pragmatic-programmer/9780135956977/f_0028.xhtml&lt;/p&gt;
&lt;p&gt;[3] Wikipedia. &lt;em&gt;Orthogonality (programming)&lt;/em&gt;. https://en.wikipedia.org/wiki/Orthogonality_(programming)&lt;/p&gt;
&lt;p&gt;[4] Zheng, L., Chiang, W.-L., Sheng, Y., et al. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena. &lt;em&gt;NeurIPS 2023&lt;/em&gt;. https://arxiv.org/abs/2306.05685&lt;/p&gt;
&lt;p&gt;[5] Evidently AI. &lt;em&gt;LLM-as-a-judge: a complete guide to using LLMs for evaluations&lt;/em&gt;. https://www.evidentlyai.com/llm-guide/llm-as-a-judge&lt;/p&gt;
&lt;p&gt;[6] Obsidian Help. &lt;em&gt;Tags&lt;/em&gt;. https://obsidian.md/help/tags&lt;/p&gt;
&lt;p&gt;[7] Obsidian Forum. &lt;em&gt;Nested tags&lt;/em&gt;. https://forum.obsidian.md/t/nested-tags/169&lt;/p&gt;
&lt;p&gt;[8] Stack Overflow Help. &lt;em&gt;What are tag synonyms and how do they work?&lt;/em&gt; https://stackoverflow.com/help/tag-synonyms&lt;/p&gt;
</content:encoded></item></channel></rss>