<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[JiaJian]]></title><description><![CDATA[远光点]]></description><link>https://jiajian233.xyz</link><image><url>https://i.stardots.io/3242053889105/StarDots-2026052915381096631.png</url><title>JiaJian</title><link>https://jiajian233.xyz</link></image><generator>Yohaku (https://github.com/Innei/Yohaku)</generator><lastBuildDate>Tue, 25 Aug 2026 11:33:10 GMT</lastBuildDate><atom:link href="https://jiajian233.xyz/feed" rel="self" type="application/rss+xml"/><pubDate>Tue, 25 Aug 2026 11:33:10 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[蜃云灯影，凡尘剑心]]></title><description><![CDATA[<link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/judvberunu8eiu3bgw.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/mjd1ghcj1b4v8u1p0c.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/d43dxnsh9lq27wwqb9.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/s0muhp1gpne9duhkf7.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/fshf9jiwtw0d8yp54t.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/tdvdkzgrz31lk821qx.png"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/notes/9">https://jiajian233.xyz/notes/9</a></blockquote><div><p><img height="1440" src="https://jiajian233.xyz/api/v2/objects/image/judvberunu8eiu3bgw.png" width="2560"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/mjd1ghcj1b4v8u1p0c.png"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/d43dxnsh9lq27wwqb9.png"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/s0muhp1gpne9duhkf7.png"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/fshf9jiwtw0d8yp54t.png"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/tdvdkzgrz31lk821qx.png"/></p></div><p style="text-align:right"><a href="https://jiajian233.xyz/notes/9#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/notes/9</link><guid isPermaLink="true">https://jiajian233.xyz/notes/9</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Sat, 22 Aug 2026 15:14:11 GMT</pubDate></item><item><title><![CDATA[飞书 Webhook AI 评分：我把上一篇文章的解法删了]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-status-gating">https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-status-gating</a></blockquote><div><p>上个月我写了篇文章，讲我搭了个飞书多维表格 AI 评分服务：有人提交需求，webhook 收到事件，大模型打分，分数写回表格，机器人给提交者发卡片。文章列了十个坑，最深的一个是写回死循环——评分本身要写表，写表又触发新事件，服务再评一次，无限循环。当时的解法是内容指纹：把参与评分的输入算个 MD5，内容没变就跳过。</p><p>这个月我把那个解法删了。不是找到了更好的哈希，而是想明白问题根本不在哈希上。新代码连让回声进入处理流程的机会都不给。</p><p>这篇是续集。讲三件事：上篇文章的解法为什么必须死；替掉它的是什么（<a href="https://github.com/kocotree/ReviewFlow">ReviewFlow</a> 现在是 v2.0.0）；以及两个比调提示词重要得多的设计决定——用状态机门控触发，而不是猜意图；对 AI 输出做严格校验，而不是打捞。</p><h2 id="">第一个被删的解法：内容指纹</h2><p>先回顾 v1 的链路。记录变更事件进来 → 收集文本、文档链接、附件 → 发给模型 → 分数和状态写回 → 通知。写回本身就是一次记录变更，于是又以新事件的形式弹回来。v1 的准入判断是&quot;内容到底变没变&quot;——对参与评分的输入算 MD5，存在内存里。</p><p>生产环境里三个雷把它炸穿了：</p><ol start="1"><li><strong>事件 payload 带的是全部字段，不是变更字段</strong>。 飞书 <code>record_changed</code> 事件的 <code>before_value</code>/<code>after_value</code> 实测是整条记录，和文档里&quot;只含变更字段&quot;的示例对不上。diff 的前提从一开始就不成立。</li><li><strong>人员字段序列化不稳定</strong>。 &quot;提报人&quot;这类字段每次读出来序列化结果都不一样，diff 恒为&quot;有变更&quot;，假阳性。自己写回的事件永远认不出来。</li><li><strong>普通文本字段也会偶发假阳性</strong>。 连文本字段都偶尔能 diff 出根本不存在的变更。</li></ol><p>就算指纹偶尔生效，它也是个带内存的启发式：进程一重启，每条记录多评一次。当时我觉得&quot;可接受&quot;。不可接受。指纹是在给一个允许系统自我触发的设计打补丁，而且靠猜。</p><p>指纹后面还藏着一层二级循环：没有可评内容（比如只传了图，模型读不了）的记录会被复位成&quot;待评分&quot;等用户补充，这次复位写回又触发新事件，而我的守卫对&quot;待评分&quot;状态开了豁免，于是&quot;无可评审→复位待评分→事件→无可评审……&quot;实测一轮约 2 秒，每秒打一次飞书 API，永不停，只能手动删记录止损。</p><h2 id="">替掉它的是什么：你没有资格触发自己</h2><p>v2 的准入逻辑是两个变量的纯函数：记录当前状态 + 谁在请求。就这些。不看内容，不比 diff，不算哈希。</p><pre class=""><code class="">状态 × 触发来源 → 是否准入
</code></pre>
<ul><li><strong>初始事件 / 崩溃恢复</strong>：只准入 <code>待评分</code>。</li><li><strong>用户重评</strong>：只准入 <code>未通过</code>，只能点卡片里的按钮，而且点击人的 <code>open_id</code> 必须和提报人字段一致。隐藏的 <code>修改轮次</code> 递增，满 5 轮进入 <code>已驳回</code>，按钮消失。</li><li><strong>管理员重试</strong>：只准入 <code>评分异常</code>，只能来自管理员群的卡片（<code>chat_id</code> 必须匹配）。</li><li><strong>其余一切</strong>：静默拒绝。</li></ul><p>以前制造回声的那次写回，现在落进 <code>已通过</code> / <code>未通过</code> / <code>评分异常</code>——这三个状态对初始事件来源全部不可触发。门控没有放行的分支，循环在结构上就形不成。没有启发式可以猜错。</p><p>诀窍就一句话：<strong>把写回结果放进事件源碰不了的状态里，回声靠设计吸收，不靠检测硬扛</strong>。</p><p style="padding:6px 12px;border-left:2px solid #C56473;background:#C5647350;font-style:italic;font-weight:500">Not support render this content in RSS render</p>
<p>光有状态门控还不够，世界不是单线程的。三个机制把它补严：</p><p><strong>Fencing</strong>。 每条记录的任务拿到一个递增的 fence 序号。最终写回前检查 <code>is_current(key, fence)</code>，如果已经被更新的任务接管（或评分过程中用户重评了），旧结果直接作废——僵尸写回盖不掉新分数。对应的回归测试叫 <code>test_fencing_discards_zombie_result_before_final_write</code>。</p><p><strong>每记录串行 + 幂等</strong>。 完整记录键（<code>app_token:table_id:record_id</code>）同时只允许一个在岗任务；webhook 的 <code>event_id</code> 在 300 秒滑动窗口里去重。卡片回调用 <code>message_id + 操作人 + 动作值</code> 派生幂等键，连点两次&quot;重新评分&quot;只会产生一个任务，重复投递自动折叠。</p><p><strong>清道夫兜底崩溃场景</strong>。 fencing 和串行都在内存里，进程被杀就丢。清道夫每 60 秒扫一次卡在 <code>评分中</code> 的记录，系统 <code>last_modified_time</code> 超过 900 秒且<strong>没有活任务</strong> 的，复位成待评分重新准入。注意那个&quot;没有活任务&quot;——跑得慢但还活着的评分永远不会被复位，哪怕时间戳看起来很旧。<code>test_slow_live_task_is_never_reset_even_when_timestamp_is_old</code> 把这条钉死了。</p><p>准入矩阵可以直接抄走：</p><table><thead><tr><th> 记录状态 \ 触发来源 </th><th> 初始事件 </th><th> 用户重评 </th><th> 管理员重试 </th><th> 清道夫 </th></tr></thead><tbody><tr><td> 待评分 </td><td> ✓ </td><td> — </td><td> — </td><td> ✓ </td></tr><tr><td> 评分中 </td><td> ✗ </td><td> ✗ </td><td> ✗ </td><td> ✗（有活任务） </td></tr><tr><td> 未通过 </td><td> ✗ </td><td> ✓（仅本人，轮次&lt;5） </td><td> — </td><td> — </td></tr><tr><td> 已通过 </td><td> ✗ </td><td> ✗ </td><td> — </td><td> — </td></tr><tr><td> 已驳回 </td><td> ✗ </td><td> ✗ </td><td> — </td><td> — </td></tr><tr><td> 评分异常 </td><td> ✗ </td><td> — </td><td> ✓（仅管理员群） </td><td> — </td></tr></tbody></table><h2 id="json-">第二个被删的解法：JSON 打捞</h2><p>v1 解析模型返回用了三级降级：直接 <code>json.loads</code> → 正则抠最外层 <code>{}</code> → 逐字段正则捞。每一级成功都会产出一个&quot;分数&quot;，其中一些内部根本对不上账。典型故障：长 <code>detail</code> 撞上 <code>max_tokens</code> 被截断，JSON 后半截非法，于是降级分别捞 <code>score</code> 和四个维度分……加起来不等于总分。系统返回了一个错的数字，打上 <code>_parse_fallback</code> 标记，继续运行。</p><p>当时管这叫健壮性。它比失败更糟：<strong>静默降级没有告警</strong>。 崩溃有堆栈，打捞出来的分数什么都没有——评分质量悄悄烂掉，没有任何信号。</p><p>v2 把方向反过来：</p><ul><li><strong>让截断不发生</strong>。 <code>max_tokens</code> 调到 4000，schema 又把内容压住（detail ≤ 500 字、highlights ≤ 150、improvements ≤ 250），正常响应是段短 JSON，留足余量。</li><li><strong>全量校验</strong>。 响应用 Pydantic 严格模式解析：字段精确、类型精确、<code>score</code> 必须等于四维度之和（<code>model_validator</code> 强制）、维度在各自区间内、<code>extra=&quot;forbid&quot;</code>。校验不过就拒绝，绝不修复。</li><li><strong>只允许两种语法恢复</strong>。 剥一层 Markdown 代码围栏，或者从前后废话里提取一个完整 JSON 对象。字段级打捞删除——测试名就叫 <code>test_parse_response_rejects_non_json_without_field_salvage</code>。</li><li><strong>响亮地失败，然后升级</strong>。 非法响应抛异常，工作流重试最多 3 次，仍失败进 <code>评分异常</code>，通知管理员群而不是提交者，修改轮次不加。</li></ul><p>原则：先让输入不容易坏（token 余量、temperature 0、提示词里写清 JSON schema），真坏了就响亮地失败到有人盯的通道里。永远不要静默返回一个没有意义的数字。</p><table><thead><tr><th> </th><th> v1 三级降级 </th><th> v2 严格校验 </th></tr></thead><tbody><tr><td> 恢复策略 </td><td> 直接 <code>json.loads</code> → 正则抠 <code>{}</code> → 逐字段正则 </td><td> 剥一层代码围栏 → 提取一个完整 JSON 对象 </td></tr><tr><td> 失败处理 </td><td> 填默认值、打 <code>_parse_fallback</code> 标记、继续跑 </td><td> 抛错 → 重试最多 3 次 → <code>评分异常</code> + 管理员告警 </td></tr><tr><td> 坏数据 </td><td> 静默产出（分数与维度之和对不上） </td><td> 拒绝（<code>model_validator</code> 强制相等） </td></tr><tr><td> 根因 </td><td> 被掩盖 </td><td> 暴露（<code>max_tokens=4000</code> 从源头防截断） </td></tr></tbody></table><h2 id="">上篇文章里没删的坑</h2><p>不是所有东西都被删了。四个坑原样保留，因为它们就是飞书的真实行为：</p><ul><li><strong>订阅 API 是硬前提</strong>。 控制台勾选事件类型不会推任何东西，必须对具体多维表格调一次 <code>drive/v1/file/subscribe</code>。零报错，静默，仍然是&quot;事件收不到&quot;的头号原因。</li><li><strong>没有顶层 <code>record_id</code></strong>。 ID 在 <code>action_list[i].record_id</code> 里，一个事件可能带多条 action。</li><li><strong>FastAPI 把 header 转小写，SDK 不认</strong>。 转交 <code>lark-oapi</code> 前把 <code>X-Lark-*</code> 复原成规范大小写，否则每次签名校验都失败。</li><li><strong>wiki 链接给的是节点 token，不是文档 token</strong>。 导出接口直接拒收（<code>1069914</code>），必须先 <code>wiki/v2/space/get_node</code> 解析，还要给应用开 <code>wiki:wiki:readonly</code>，否则静默降级。</li></ul><p>两个坑换了形态。日期字段要毫秒时间戳那个坑没了——v2 干脆不写评分时间字段，detail、亮点、改进建议只进通知卡片，不进表。图片占位符污染评分的坑也没了，但原因更彻底：v2 把 <code>raw_content</code> 纯文本整个移除了。</p><h2 id="">替掉纯文本的内容管线</h2><p>两篇文章之间最大的架构变化：评分输入从&quot;你抓来的文本&quot;变成&quot;你造出来的一份 PDF&quot;。所有在线文档经导出任务 API 转 PDF，所有附件转 PDF（图片走 Pillow，处理 EXIF 方向、压平透明通道；Word/Markdown/纯文本走无头 LibreOffice，独立 profile、60 秒硬超时、超时杀进程组），然后合并成一份确定性总 PDF——文档在前、附件在后，每份材料前有来源分隔页，按解析后的真实 token / <code>file_token</code> 去重。</p><p>这一下消灭了一类 bug（模型看到的就是用户看到的：排版、截图、表格），也带来一批必须一起上线的限制：</p><ul><li>附件最多 20 个、单个 20MB、总共 100MB、PDF 最多 300 页、图片最多 20 张，能提前查的都在下载前查。</li><li>加密或损坏的 PDF 是<strong>用户可修复的材料问题</strong>，不是技术故障：提交者收到一张列明全部问题文件的卡片，记录落 <code>未通过</code>，不烧修改轮次。</li><li>任何一份文档导出或附件转换失败，整次采集中止——没有部分 PDF，没有&quot;差不多得了&quot;的降级。<code>test_transient_retry_repeats_only_failed_download_step</code> 证明重试按步骤隔离，<code>test_pdf_bundle_failure_never_calls_ai_or_writes_partial_result</code> 证明 AI 永远不会收到残缺的 PDF。</li><li>启动时校验：模型不支持 PDF 文件输入、或 LibreOffice 不在，直接拒绝启动。开机就失败，而不是凌晨两点静默降级。</li></ul><p>还有两条生产经验值得抄：</p><p><strong>写工作流之前先给失败分类</strong>。 每个错误都有类别：瞬时故障（重试最多 3 次）、用户可修复的材料问题（→ <code>未通过</code> + 列明哪些文件坏了的卡片）、系统硬失败（→ <code>评分异常</code>，只进管理员群，提交者永远看不到技术报错卡片）。上面那张准入矩阵之所以成立，靠的就是这套分类。</p><p><strong>熔断发送侧，不熔断提报侧</strong>。 单条记录 5 分钟窗口最多 20 张卡片，超限停发并给管理员群发一次告警。正常提报永远不限流，熔断只拦&quot;一条记录不断产卡片&quot;的病态循环。</p><h2 id="">动手之前先问三个问题</h2><p>如果你要把 AI 接进飞书、Airtable、Notion，或者任何&quot;你的服务既消费事件又往同一个存储里写&quot;的平台，先跑一遍这个：</p><ol start="1"><li><strong>我是不是这张表的写回者兼消费者？</strong> 不是，普通幂等（按 event_id 去重）就够了。是，往下看。</li><li><strong>我的写回能不能落进事件源碰不了的状态？</strong> 能，就做状态门控 + 显式人工动作（按钮、管理员重试）——这是让回声在结构上不可能发生的设计。</li><li><strong>如果不能</strong>（比如 A 表写回必须立刻驱动 B 表的处理），门控关不掉回声，这时才轮到指纹/幂等键，而且要把它们当启发式用：预期假阳性，预期重启丢内存，再加个清道夫兜底。</li></ol><p>AI 输出侧同理：<strong>优先&quot;不容易坏&quot;，而不是&quot;坏了再捞&quot;</strong>。 token 余量、确定性采样、提示词里写清 JSON schema、严格校验、响亮失败带重试、升级到人工通道。你要是发现自己正在写第四个解析兜底，停下来问一句前三个为什么存在。</p><p style="padding:6px 12px;border-left:2px solid #C56473;background:#C5647350;font-style:italic;font-weight:500">Not support render this content in RSS render</p>
<h2 id="">什么时候这套不适用</h2><p>状态门控模式的前提是：流程能建模成少量状态 + 少量触发来源。它失效的情况：需要连续自由编辑持续触发重评（没有离散状态可以设门）；多实例部署但没把串行和 fencing 外置（内存守卫是单实例假设）；写回者/消费者跨表分离（问题 2 的答案是&quot;不能&quot;）。这些场景里指纹方案不算错——它只是启发式，你应该预算它的失败模式，而不是像我一样在生产里踩出来。</p><hr/><p><strong>相关阅读</strong>：这个项目的上一篇，<a href="/posts/project/lark-webhook-ai-judge-pitfalls/">飞书 webhook AI 评分踩坑清单</a>，把平台坑讲得很细。LLM 评委那部分，评分提示词在<a href="https://github.com/kocotree/ReviewFlow">仓库</a>的 <code>app/ai.py</code> 里，飞书官方文档的<a href="https://open.feishu.cn/document/server-docs/docs/drive-v1/file/subscribe">文件订阅</a>和<a href="https://open.feishu.cn/document/server-docs/docs/drive-v1/export_task/create">文档导出</a>是两个最先接触的 API 的权威参考。</p></div><p style="text-align:right"><a href="https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-status-gating#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-status-gating</link><guid isPermaLink="true">https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-status-gating</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Tue, 11 Aug 2026 05:23:57 GMT</pubDate></item><item><title><![CDATA[基于飞书多维表格的需求审核评分服务]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-pitfalls">https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-pitfalls</a></blockquote><div><p>我给飞书多维表格搭了个 AI 自动评分服务:有人往表里提交待评审的内容,服务监听到记录变更,把文本、飞书文档、附件全收集起来喂给大模型打分,再把分数、评语、通过/未通过状态写回表格,顺带用机器人给提交者发条通知。</p><p>听起来是个&quot;接个 AI API 就完事&quot;的活。真做下来,我在生产里踩了 10 个坑,其中 9 个和 AI 的智能程度毫无关系。</p><p>它们分两类。一类是平台不按你以为的方式工作,飞书 webhook 有一堆反直觉行为;另一类是 LLM 当裁判天生放水,你不摁住它,它就给同情分。真正调通&quot;让模型打个分&quot;这件事,大概占了我 10% 的时间。</p><p>这不算一篇夸 no-code + AI 有多神的文章,更像一份过来人把坑摊开给你看的清单:每个坑我给你现象、根因,和代码级的修复。如果你正打算在飞书、Airtable、Notion 这类平台上接 AI 做自动化,这些坑大概率也在等你。</p><h2 id="">这套流水线到底在做什么</h2><p>先把架构讲清楚,后面的坑才有坐标。整条链路是事件驱动的:飞书表里一条记录变了 → webhook 收到事件 → 一个编排器(orchestrator)把这条记录的所有内容凑齐 → 发给 AI 打分 → 结果写回表格 → 通知提交者。</p><p style="padding:6px 12px;border-left:2px solid #C56473;background:#C5647350;font-style:italic;font-weight:500">Not support render this content in RSS render</p>
<p>注意那条虚线:写回结果本身会触发一个新的记录变更事件。这条不起眼的回边是全文最深的坑,后面第二幕专门讲。</p><p>技术栈很朴素:FastAPI 收 webhook,<code>lark-oapi</code> SDK 跟飞书打交道,大模型用豆包(Doubao),<code>httpx</code> 下载文件。没有一样是花活。坑不来自技术选型,坑来自平台的真实行为和文档描述之间的那道缝。</p><p>下面按我踩坑的顺序,分四幕讲。</p><h2 id="">第一幕:平台不按你以为的方式工作</h2><p>飞书 webhook 的头四个坑有个共同点:每一个都是闷声失败。代码没报错、服务正常跑、日志一片祥和,但事件就是收不到,或者字段就是写不进去。没有异常栈给你指路,你只能靠猜。这是最耗时间的一类 bug。</p><h3 id="-1-url">坑 1:配了回调 URL,事件却一条都收不到</h3><p>我最早卡了大半天在这:webhook 地址配好了、事件类型在控制台勾了、握手验证也过了,可是往表里改记录,服务端一个事件都收不到。</p><p>根因反直觉到离谱:飞书云文档事件,光在控制台订阅是不够的,你必须用代码主动调一次订阅 API,去&quot;激活&quot;对某个具体文档的订阅。控制台里那个勾更像是&quot;授权&quot;,真正的订阅动作得你自己发:</p><pre class="language-python lang-python"><code class="language-python lang-python">from lark_oapi.api.drive.v1 import SubscribeFileRequest

req = SubscribeFileRequest.builder() \
    .file_token(&quot;&lt;BITABLE_APP_TOKEN&gt;&quot;) \
    .file_type(&quot;bitable&quot;) \
    .build()
client.drive.v1.file.subscribe(req)
</code></pre>
<p>这段代码不在我的服务主流程里,它是部署前的一次性手动操作,得对每个要监听的多维表格执行一次。我特意提这点,是因为你翻遍服务代码也找不到它,却又非它不可。需要 <code>docs:event:subscribe</code> 或 <code>drive:drive</code> 权限。</p><blockquote><p><strong>避坑</strong>:如果你的 webhook 握手能过、但业务事件收不到,先别怀疑代码,去确认这个订阅 API 调没调过。这一步没有任何报错提示你漏了它。</p></blockquote>
<h3 id="-2-payload--recordid">坑 2:事件 payload 里没有顶层 record_id</h3><p>订阅通了,事件能收到了,下一个坑立刻来:我按直觉去 <code>event.record_id</code> 取记录 ID,拿到的是 <code>None</code>。</p><p>飞书多维表格变更事件(<code>P2DriveFileBitableRecordChangedV1</code>)根本没有顶层 record<em>id。ID 藏在 `action</em>list` 里,而且一个事件可能携带多条 action(新增/编辑/删除混在一起),得遍历:</p><pre class="language-python lang-python"><code class="language-python lang-python"># action_list 里才是变更的记录,一个事件可能有多条
action_list = getattr(event, &quot;action_list&quot;, []) or []
for action in action_list:
    record_id = getattr(action, &quot;record_id&quot;, None)
    action_type = getattr(action, &quot;action&quot;, &quot;&quot;) or &quot;&quot;
    if not record_id:
        continue
    # ... 逐条处理
</code></pre>
<p>这个坑的隐蔽之处在于:SDK 自动生成的事件模型里,<code>record_id</code> 这个属性名压根不在你以为的层级,IDE 补全也不会提示你去 <code>action_list</code> 里找。批量编辑时一个事件带多条 action,也是个容易漏的点:你要是只取第一条,批量操作就丢记录了。</p><h3 id="-3fastapi--header-sdk-">坑 3:FastAPI 把 header 全转小写,SDK 却要原始大小写</h3><p>这个坑更阴,因为它表现成签名验证失败,而不是&quot;header 找不到&quot;。</p><p>飞书用 <code>X-Lark-Request-Timestamp</code>、<code>X-Lark-Request-Nonce</code>、<code>X-Lark-Signature</code> 这几个 header 做签名校验。而 FastAPI 的 <code>dict(request.headers)</code> 会把所有 header 名转成全小写(HTTP 标准允许),<code>lark-oapi</code> SDK 却用大小写敏感的方式去查它们:查不到,拿到 <code>None</code>,签名计算直接崩,所有事件被拒。</p><p>修复就是在把请求转交给 SDK 之前,手动把这几个签名 header 的大小写复原:</p><pre class="language-python lang-python"><code class="language-python lang-python">raw_headers = dict(request.headers)   # 已被 FastAPI 转成全小写
headers: dict[str, str] = {}
for k, v in raw_headers.items():
    kl = k.lower()
    if kl == &quot;x-lark-request-timestamp&quot;:
        headers[&quot;X-Lark-Request-Timestamp&quot;] = v
    elif kl == &quot;x-lark-request-nonce&quot;:
        headers[&quot;X-Lark-Request-Nonce&quot;] = v
    elif kl == &quot;x-lark-signature&quot;:
        headers[&quot;X-Lark-Signature&quot;] = v
    else:
        headers[k] = v
</code></pre>
<p>只需复原这三个签名相关的,其余 header 不敏感。这个坑的教训是:当一个 Web 框架和一个 SDK 对同一份 HTTP 元数据有不同假设时,缝就夹在中间。两边各自都&quot;没错&quot;,但拼一起就漏。</p><h3 id="-4-iso-">坑 4:日期字段要毫秒时间戳,不是 ISO 字符串</h3><p>前三个坑卡在&quot;收事件&quot;,这个坑卡在&quot;写回去&quot;。</p><p>我要把评分时间写进多维表格的日期字段,顺手塞了个 ISO 8601 字符串(<code>2026-07-13T14:30:00Z</code>),飞书默默拒绝,字段写不进去,还不给你清晰的报错。飞书 Bitable 的日期/时间字段吃的是毫秒级 Unix 时间戳(整数):</p><pre class="language-python lang-python"><code class="language-python lang-python">now = int(datetime.now(timezone.utc).timestamp() * 1000)  # 例如 1752408000000

update_fields = {
    FIELD_AI_SCORE: score,
    FIELD_AI_SCORE_TIME: now,   # 毫秒 int,不是 ISO 字符串
    FIELD_SCORE_STATUS: new_status,
    # ...
}
</code></pre>
<p>必须是 <code>int</code>,不能是 float、更不能是字符串。这类&quot;格式对了才写得进、错了也不明说&quot;的坑,飞书里散落着好几处(附件下载的认证也是一个,后面第三幕会讲)。</p><p><strong>第一幕小结</strong>:这四个坑没有一个需要你懂 AI。它们是 no-code + AI 自动化里那部分&quot;没人会在 demo 里提&quot;的隐藏工时:平台的真实契约和文档、直觉之间的每一道缝,你都得亲自摔一次才知道。而下一个坑,比这四个加起来都深。</p><h2 id="">第二幕:最深的坑——写回把自己打进死循环</h2><p>这是我在这个项目上花时间最多、也最能说明&quot;事件驱动 + 自动写回&quot;到底难在哪的一个坑。它不是一个 bug,是一串层层嵌套的 bug:每修好一层,下一层就冒出来。</p><h3 id="">病根:写回会触发一个新事件</h3><p>回到开头那张流程图的虚线。评分完成后,我把分数、评语、状态写回记录。这个写回操作本身,在飞书看来就是一次&quot;记录变更&quot;,于是又推给我一个 webhook 事件。服务收到这个事件,又开始评分,又写回,又触发……</p><p style="padding:6px 12px;border-left:2px solid #C56473;background:#C5647350;font-style:italic;font-weight:500">Not support render this content in RSS render</p>
<p>如果评分结果落在一个&quot;可触发评分&quot;的状态(比如&quot;未通过&quot;,用户改完还得再评),那这个回声就会稳稳地把系统拖进无限循环。第一次上线,我眼睁睁看着一条记录被反复评分,豆包的调用量和飞书的 API 请求量一起往上飙。</p><h3 id="-diff-">第一次尝试:用字段级 diff 识别写回,被击穿</h3><p>最直觉的解法:事件里带了变更前后的字段值,那我比一下 diff。如果只有输出字段(分数/状态)变了、输入字段没变,就判定是我自己的写回,跳过。</p><p>这个方案在真实表里被彻底击穿,踩了三个雷:</p><ol start="1"><li>payload 携带的是全部字段,不是变更字段。飞书事件里 <code>action.after_value</code> / <code>before_value</code> 实测带的是记录的所有字段,和官方文档给的&quot;只含变更字段&quot;示例矛盾。diff 的基础前提就不成立。</li><li>人员字段序列化不稳定。&quot;提报人&quot;这类人员字段每次序列化的结果都不完全一样,diff 因此恒为&quot;有变更&quot;,假阳性,写回永远被误判成用户编辑。</li><li>文本字段偶发假阳性。连普通文本字段都会偶尔 diff 出不存在的变更。</li></ol><p>三个雷叠加,字段级识别彻底不可靠。这是我从官方文档示例出发、结果被真实数据打脸的一次:文档描述的事件模型,和你的表实际推送的,不是一回事。</p><h3 id="">正解:内容指纹</h3><p>既然没法靠&quot;谁变了&quot;判断,那就靠&quot;内容本身变没变&quot;判断。我给每条记录算一个内容指纹:只取真正参与评分的字段(原始描述、需求文档链接、需求附件),算个 MD5。附件用稳定标识(<code>file_token</code>,退化时用文件名+大小),刻意避开 <code>tmp_url</code> 这种每次拉取都变的易变值,保证同一份内容多次读取,指纹恒定。</p><pre class="language-python lang-python"><code class="language-python lang-python">def _content_signature(self, fields: dict) -&gt; str:
    text = str(fields.get(FIELD_TEXT_CONTENT, &quot;&quot;) or &quot;&quot;)

    doc = fields.get(FIELD_DOC_LINK, &quot;&quot;)
    if isinstance(doc, dict):
        doc = doc.get(&quot;link&quot;, &quot;&quot;) or doc.get(&quot;text&quot;, &quot;&quot;)
    doc = str(doc or &quot;&quot;)

    atts = fields.get(FIELD_ATTACHMENT, []) or []
    att_parts = []
    for a in atts if isinstance(atts, list) else []:
        if isinstance(a, dict):
            # file_token 稳定;退化时用 名字+大小,避开每次都变的 tmp_url
            att_parts.append(str(
                a.get(&quot;file_token&quot;) or f&quot;{a.get(&#x27;name&#x27;,&#x27;&#x27;)}:{a.get(&#x27;size&#x27;,&#x27;&#x27;)}&quot;
            ))

    raw = &quot;&quot;.join([text, doc, *att_parts])
    return hashlib.md5(raw.encode(&quot;utf-8&quot;)).hexdigest()
</code></pre>
<p>逻辑很简单:处理前算一次指纹,和上次存的比。一样,是我自己写回的回声,跳过;不一样,用户真改了内容,重评。指纹存内存,重启后每条至多多评一次,可接受。</p><pre class="language-python lang-python"><code class="language-python lang-python">content_sig = self._content_signature(fields)
if self._scored_sig.get(record_id) == content_sig:
    logger.info(&quot;跳过写回回声(评分内容未变化): record=%s&quot;, record_id)
    return
self._scored_sig[record_id] = content_sig
</code></pre>
<p>用&quot;内容变没变&quot;代替&quot;字段 diff&quot;,绕开了人员字段序列化不稳定的假阳性。用户改内容,指纹变,自然重评;单纯的状态写回,指纹不变,一律跳过。</p><h3 id="">二级坑:空内容记录停在&quot;待评分&quot;态,又循环了</h3><p>以为搞定了,结果又冒出一层,这层更狡猾。</p><p>我早期的守卫写的是 <code>if status != 待评分 and 指纹相同: 跳过</code>。加那个 <code>status != 待评分</code> 的豁免,是想着&quot;待评分一定是用户显式想重新评,不该当回声跳过&quot;。这个假设是错的。</p><p>问题出在两个分支上:一条记录没有可评审内容(比如只上传了图,而模型不支持图),或者附件格式不认,系统会把状态恢复成&quot;待评分&quot;等用户补充。可是,恢复&quot;待评分&quot;的这次写回又触发了一个事件;因为 <code>status == 待评分</code> 命中了豁免,回声没被跳过;于是又&quot;无可评审→恢复待评分→事件→无可评审……&quot;</p><p>实测:每 ~2 秒一轮,永不停止。不发 AI(空内容分支在调模型前就返回了),通知被冷却挡着,但它持续打飞书 API,约 1 次/秒,靠自身逻辑永远不会停。止损只能手动删记录。</p><p>修复很干脆:去掉那个豁免,回声守卫改成纯指纹比对:</p><pre class="language-python lang-python"><code class="language-python lang-python"># 改前:if status != STATUS_PENDING and 指纹相同: return   # 待评分被豁免 → 漏
# 改后:
if self._scored_sig.get(record_id) == content_sig:
    return
</code></pre>
<p>去掉 <code>status != 待评分</code> 后,首次到达仍会正常处理(此时还没指纹缓存,比对不相等),之后相同内容(含&quot;待评分&quot;态的空内容回声)一律跳过。实测:空内容记录只处理一次就稳住,死循环根除;而未通过的记录改成充实内容后,score 从 0 重新评到 79、轮次 1→2,没被误判成回声。</p><h3 id="">这一幕真正的教训</h3><p>写回死循环这一串,本质是事件驱动自动化的一个结构性陷阱:只要你的系统既是事件的消费者、又是事件源的写入者,它就会听到自己的回声。飞书这个案例把它放大了,因为你连&quot;这是不是我自己的回声&quot;都没法可靠判断(字段 diff 被击穿)。</p><p>解法的通用形态值得记住:别问&quot;谁改的&quot;,问&quot;实质内容变了没有&quot;。给你真正在乎的输入算一个稳定指纹,拿指纹当幂等键。这个思路能迁移到任何&quot;写回会触发再处理&quot;的自动化场景,不只是飞书。</p><h2 id="-ai-">第三幕:喂给 AI 之前,内容是脏的</h2><p>前两幕在跟&quot;事件&quot;较劲。这一幕跟&quot;内容&quot;较劲:你从飞书拿到的内容,不是你以为的那份干净内容。这些坑不会让服务崩,而是会悄悄拉低评分质量,比崩溃更难发现,因为一切都&quot;看起来正常&quot;。</p><h3 id="-7-imagepng">坑 7:文档里的图片,变成了污染评分的 <code>image.png</code></h3><p>这个坑我排查了半天,因为它表现成一个自相矛盾的现象:缓存里的文档文本明明很干净,评分详情却在抱怨&quot;文档里有一堆 <code>image.png</code> 冗余占位符,格式扣分&quot;。文本里没有的东西,模型怎么会抱怨?</p><p>根因:飞书文档的 <code>raw_content</code> 接口会把每一张内嵌图片渲染成一个独占一行的裸文件名,截图或粘贴图默认就叫 <code>image.png</code>,也可能是 <code>image (1).png</code>、<code>截图.jpg</code>。这些既不是用户写的内容,又被评分模型当成&quot;排版里塞了一堆冗余占位符&quot;莫名扣格式分。</p><p>而那个&quot;自相矛盾&quot;是这么来的:转写缓存用的提示词明令禁止输出占位符,把它们剔了,所以缓存干净;评分走的是另一条路,看到了带占位符的原文。同源内容,两套提示词,制造了一个假矛盾。</p><p>修复是在内容进评分和缓存之前,统一清洗掉&quot;整行就是一个图片文件名&quot;的行:</p><pre class="language-python lang-python"><code class="language-python lang-python">_IMAGE_PLACEHOLDER_LINE = re.compile(
    r&quot;^[\w一-鿿.\-()（） ]{1,80}\.(?:png|jpe?g|gif|webp|bmp|svg|tiff?)$&quot;,
    re.IGNORECASE,
)

def strip_image_placeholders(text: str) -&gt; str:
    if not text:
        return text
    kept = [ln for ln in text.splitlines()
            if not _IMAGE_PLACEHOLDER_LINE.match(ln.strip())]
    cleaned = &quot;\n&quot;.join(kept)
    return re.sub(r&quot;\n{3,}&quot;, &quot;\n\n&quot;, cleaned).strip(&quot;\n&quot;)   # 顺手压掉多余空行
</code></pre>
<p>关键分寸:只删&quot;整行就是图片文件名&quot;的行,保留正文里顺带提到文件名的句子(比如&quot;详见附件 <code>方案.png</code>&quot;),也保留 <code>.docx</code> 这类非图片名。实测拿真实 wiki 文档跑,占位符从 4 行清到 0 行,评分详情不再无端抱怨格式。</p><blockquote><p><strong>通用教训</strong>:任何&quot;富文本导出成纯文本&quot;的接口,都会往文本里掺你没预期的渲染产物(图片占位符、表格边框字符、脚注标记)。进 LLM 之前务必过一道清洗,否则模型会把这些噪声当成内容质量的一部分来评判。</p></blockquote>
<h3 id="-8wiki--token-token">坑 8:wiki 链接里的 token,不是文档 token</h3><p>飞书文档链接有好几种形态。普通文档是 <code>/docx/&lt;token&gt;</code>,知识库链接是 <code>/wiki/&lt;node_token&gt;</code>。这个 <code>node_token</code> 是知识库&quot;节点&quot;的 token,不是背后真实文档的 token。</p><p>坑在于:<code>raw_content</code> 接口能直接吃 wiki 节点 token(内部帮你自动解析了),于是纯文本读取一切正常,你毫无察觉。但导出 PDF 的 <code>export_task</code> 接口不认:直接拿节点 token 去导,报 <code>1069914 file token invalid</code>,于是永远回退到纯文本,文档里的图片永远不会被解析(多模态模型看不到图)。</p><p>修复是导出前先把 wiki 节点解析成真实文档 token:</p><pre class="language-python lang-python"><code class="language-python lang-python"># wiki 链接的 token 是节点 token,导出前必须先解析出挂载的真实文档 obj_token
req = GetNodeSpaceRequest.builder().token(node_token).build()
resp = client.wiki.v2.space.get_node(req)
# resp.data.node.obj_token / obj_type(docx/doc/sheet...)才是能拿去导出的
</code></pre>
<p>这里还埋了个权限坑:<code>get_node</code> 需要应用有 wiki 访问权限,否则报 <code>99991672 Access denied</code>。这个权限得去飞书控制台单独给应用开(<code>wiki:wiki:readonly</code> 或 <code>wiki:node:read</code>),开通前 wiki 文档只能走纯文本、看不到图。又是一个代码写对了、但权限没配就闷声降级的坑。</p><h3 id="-9-400">坑 9:附件下载,不带认证就是 400</h3><p>顺带说个小而致命的:飞书返回的附件下载 URL,直接用 <code>httpx.get()</code> 裸拉会 400。必须带上租户级 access token:</p><pre class="language-python lang-python"><code class="language-python lang-python">token = TokenManager.get_self_tenant_token(self._client._config)
headers = {&quot;Authorization&quot;: f&quot;Bearer {token}&quot;}
resp = await self._http.get(url, headers=headers)
</code></pre>
<p>飞书所有文件资源的下载都得带这个 header。它和坑 4(datetime 格式)是同一类:平台的隐性契约,文档里一笔带过,漏了就失败,而且报错不告诉你真正原因。</p><h3 id="-10ai--json--jsonloads-">坑 10:AI 返回的 JSON 会破,直接 <code>json.loads</code> 就崩</h3><p>内容干净了,喂给模型,模型该老老实实还我一个 JSON 评分了吧?也不一定。</p><p>我让模型返回 <code>{score, detail, dimensions}</code> 结构。但豆包在边界情况下会返回破碎的 JSON,最典型的是 <code>detail</code>(评语,有时还带文档转写)很长,撞上 <code>max_tokens</code> 被从中间截断,末尾的 JSON 直接非法。这时候 <code>json.loads()</code> 一抛异常,整个评分流程终止,用户永远等不到结果。</p><p>解法是三级降级解析,能捞多少捞多少:</p><pre class="language-python lang-python"><code class="language-python lang-python">def _parse_response(self, text: str) -&gt; dict:
    # 一级:直接解析,正常情况 95% 命中
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # 二级:正则抠出最外层 {...} 再解析,容忍模型加的前后缀废话
    m = re.search(r&quot;\{[\s\S]*\}&quot;, text)
    if m:
        try:
            return json.loads(m.group())
        except json.JSONDecodeError:
            pass

    # 三级:JSON 仍破(通常是尾部被截断),逐字段正则捞回 score/detail/维度分
    score_match = re.search(r&#x27;&quot;score&quot;[\s:]*(\d+)&#x27;, text)
    score = max(0, min(100, int(score_match.group(1)) if score_match else 0))
    # ... 各维度同理,捞不到才退 0
    return {&quot;score&quot;: score, ..., &quot;_parse_fallback&quot;: True}
</code></pre>
<p>三级的设计有个讲究:为什么不直接在解析失败时把维度分填 0?因为响应被截断时,靠前的 <code>score</code> 和各维度分往往是完整可读的,只有靠后的长 <code>detail</code> 被切了。硬填 0 会造成 <code>score</code> 和四个维度分之和对不上,一个明显的数据不一致。逐字段正则捞,能保住这份一致性。结果里标个 <code>_parse_fallback: True</code>,方便事后统计降级率。</p><p><strong>第三幕小结</strong>:这一幕的四个坑,没有一个会让你的服务挂掉,它们只会让评分悄悄变差或悄悄降级。这比崩溃危险,因为崩溃有栈、有告警,而&quot;评分质量慢慢烂掉&quot;没有任何信号。接 AI 做自动化,输入侧的数据清洗和输出侧的容错解析,值得的投入远超你的预期。</p><h2 id="ai-">第四幕:AI 当评委,天生放水</h2><p>前三幕的坑都在 AI 外围。这一幕才碰到 AI 本身,而它暴露的问题可能是这套系统最反直觉的一点:大模型当评委,默认是个老好人。</p><h3 id="-80-">症状:&quot;把要求做完了&quot;就能拿 80 分</h3><p>第一版提示词我写得挺&quot;正经&quot;:要求模型&quot;严格、客观、可复现,发现缺陷就扣分&quot;。上线一跑,分数普遍虚高,只要内容不是空的、把要求大致做完了,轻松 80 分往上。想分出&quot;哪份该打 60、哪份该打 90&quot;,分数却挤在一起分不开。</p><p>问题不在模型笨,在于 LLM 的默认人格是讨好型的。你说&quot;严格&quot;,它理解成&quot;别太苛刻&quot;;你让它评价,它倾向于找优点、给&quot;努力分&quot;和&quot;同情分&quot;。中性、礼貌的措辞,喂出来的就是中性偏高、区分度低的分数。这不是 prompt 写得不够详细,是方向就没摁住。</p><h3 id="">解法:从&quot;印象给分&quot;逼成&quot;逐条扣分&quot;</h3><p>我把系统提示词重写了一版,核心是换掉模型的给分心智模型。对比一下改动前后的措辞:</p><table><thead><tr><th> 维度 </th><th> 改前(放水版) </th><th> 改后(收紧版) </th></tr></thead><tbody><tr><td> 基准心态 </td><td> &quot;严格、客观、发现缺陷就扣分&quot; </td><td> &quot;近乎苛刻,对标专业交付、可直接投入使用;宁可偏低,绝不偏高&quot; </td></tr><tr><td> 给分方式 </td><td> 逐维度对照标尺定档 </td><td> 默认从满分起扣,每发现一处缺陷即扣,逐条列缺陷、逐条标扣分 </td></tr><tr><td> &quot;做完了&quot;值几分 </td><td> (未明确) </td><td> &quot;把要求做完&quot;只是及格线附近,高分必须由超出预期的优点支撑 </td></tr><tr><td> 硬性上限 </td><td> 无 </td><td> 引入红线:事实错误 ≤60、关键缺失 ≤55、跑题 ≤40、敷衍 ≤25 </td></tr><tr><td> 注水内容 </td><td> (未明确) </td><td> 空话套话、重复堆砌、为凑数而写,不仅不加分,还因稀释信息密度倒扣 </td></tr></tbody></table><p>三个改动最关键。</p><p>一是&quot;从满分起扣的扣分账&quot;。让模型别再&quot;看一眼给个印象分&quot;,而是从 100 分开始,强制逐条列出缺陷、逐条标注扣多少。这把打分从一个模糊的直觉动作,变成了一个可追溯的清算过程,顺带评语也更有理有据了。</p><p>二是&quot;硬性上限红线&quot;。有些缺陷是致命的,不该被别的维度救回来。比如内容有事实性错误、足以误导使用者,那不管排版多漂亮、篇幅多完整,整份分数封顶 60。这一条直接解决了&quot;局部亮点掩盖致命伤&quot;的虚高。</p><pre class=""><code class="">【硬性上限(红线,满足任一项则整份 score 被封顶)】
- 存在事实性错误、逻辑硬伤或自相矛盾,足以误导使用者:score ≤ 60。
- 关键要素缺失,导致内容无法据以落地/执行/理解:score ≤ 55。
- 通篇空泛、无实质信息或严重跑题:score ≤ 40。
- 内容近乎空白、敷衍了事或答非所问:score ≤ 25。
(多条红线同时命中时,取最低的上限。)
</code></pre>
<p>三是明令反注水。LLM 特别容易被&quot;看起来很努力&quot;骗到:篇幅长、态度诚恳、排版花哨,它就想加分。得显式禁止:不因篇幅、态度、排版加分,注水内容反而倒扣。</p><p>改完之后,分数分布明显拉开了,&quot;做完了&quot;落回中档,真正专业的交付才够得着高分。同一份内容重复评,分数也稳(配合 <code>temperature=0</code> 的确定性采样)。</p>
<h3 id="llm-">但要诚实:LLM 评委有天花板</h3><p>摁住放水之后,它变得能用了,但别指望它变成一个完美裁判。两个改不掉的局限得摊开说。</p><p>一是一致性只是&quot;够用&quot;,不是&quot;绝对&quot;。即便 <code>temperature=0</code>,同一份内容多次评分仍可能有轻微漂移。它适合做初筛和分档,不适合做&quot;精确到分、分数还直接决定钱和晋升&quot;的终裁。</p><p>二是它评的是&quot;文本表现&quot;,不是&quot;事实真伪&quot;。一段写得漂亮但事实错误的内容,模型不一定抓得住,除非错误明显到它的知识能覆盖。红线能兜住&quot;它发现的&quot;致命伤,兜不住&quot;它没发现的&quot;。所以我的定位始终是 AI 初筛加人工复核,而不是全自动终判。这条边界直接引出最后一幕:什么场景值得这么搭。</p>
<h3 id="-webhook--ai-">一张飞书 webhook + AI 评分踩坑速查表</h3><p>最后把这 10 个坑压成一张表,你可以截图存下来,搭之前对着排雷:</p><table><thead><tr><th> # </th><th> 坑 </th><th> 现象 </th><th> 一句话修复 </th></tr></thead><tbody><tr><td> 1 </td><td> 事件订阅没激活 </td><td> 配了回调却收不到任何事件 </td><td> 代码主动调 <code>SubscribeFile</code> API,控制台勾选不够 </td></tr><tr><td> 2 </td><td> 没有顶层 record_id </td><td> <code>event.record_id</code> 是 None </td><td> 去 <code>action_list[i].record_id</code> 遍历取,一事件可含多条 </td></tr><tr><td> 3 </td><td> header 大小写 </td><td> 签名校验失败,事件被拒 </td><td> 转交 SDK 前手动复原 <code>X-Lark-*</code> 三个 header 大小写 </td></tr><tr><td> 4 </td><td> 日期字段格式 </td><td> ISO 字符串写不进去,不报错 </td><td> 用毫秒级 Unix 时间戳(int),<code>timestamp()*1000</code> </td></tr><tr><td> 5 </td><td> 写回死循环 </td><td> 写回触发新事件,反复评分烧配额 </td><td> 内容指纹当幂等键,回声跳过(别用字段 diff) </td></tr><tr><td> 6 </td><td> 待评分态二级循环 </td><td> 空内容记录每 ~2s 自触发一轮 </td><td> 回声守卫去掉 <code>status!=待评分</code> 豁免,纯指纹比对 </td></tr><tr><td> 7 </td><td> 图片占位符污染 </td><td> 评语抱怨 <code>image.png</code> 但文本里没有 </td><td> 清洗&quot;整行就是图片名&quot;的行,进 LLM 前统一处理 </td></tr><tr><td> 8 </td><td> wiki token 陷阱 </td><td> 导出 PDF 报 <code>1069914</code>,图片永不解析 </td><td> 先 <code>get_node</code> 把 wiki 节点解析成真实文档 token </td></tr><tr><td> 9 </td><td> 附件下载 400 </td><td> 裸 <code>httpx.get</code> 拉不到文件 </td><td> 带 <code>Authorization: Bearer &lt;tenant_token&gt;</code> </td></tr><tr><td> 10 </td><td> AI JSON 破碎 </td><td> <code>json.loads</code> 崩,用户等不到结果 </td><td> 三级降级:直接→抠 <code>{}</code>→逐字段正则捞回 </td></tr></tbody></table><h3 id="">收个尾</h3><p>回到开头那个数字:10 个坑,9 个和 AI 无关。</p><p>这不是说 AI 不重要。而是说,当&quot;接个大模型&quot;变得越来越简单,真正的工程量就整个转移到了别处:转移到平台集成那些没人写进文档的隐性契约里,转移到&quot;事件驱动系统听到自己回声&quot;这类结构性陷阱里,转移到把一个讨好型 LLM 摁成一个能用的评委上。</p><p>如果你也要在飞书、Airtable、Notion 上接 AI 做自动化,别被 demo 的丝滑骗了。demo 展示的是那 10%,这篇文章讲的是等着你的那 90%。搭之前,把上面那张速查表和那段提示词骨架存下来,至少能帮你把最深的两个坑(收不到事件、写回死循环)提前绕过去。</p><hr/><p><strong>相关阅读</strong>:如果你对&quot;LLM 当评委&quot;这个话题感兴趣,可以继续了解 LLM-as-a-judge 的评测偏差,以及事件驱动架构里的幂等性设计。这两个主题在这个项目里被反复验证,值得单独深挖。</p></div><p style="text-align:right"><a href="https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-pitfalls#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-pitfalls</link><guid isPermaLink="true">https://jiajian233.xyz/posts/project/lark-webhook-ai-judge-pitfalls</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Mon, 13 Jul 2026 08:05:45 GMT</pubDate></item><item><title><![CDATA[I Really Want to Stay At Your House]]></title><description><![CDATA[<link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/1mgl0de7fqtqu93thu.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/0fq64w5rcuw89eliac.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/0bkdrfxo9otcwzk0ro.png"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/4bck1wi6m8hdnb2ta4.png"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/notes/7">https://jiajian233.xyz/notes/7</a></blockquote><div><p>JGN 评分：8分</p><p>配队不轮椅 -1，我打不过 6 级重锤 -1。</p><p><img height="1440" src="https://jiajian233.xyz/api/v2/objects/image/1mgl0de7fqtqu93thu.png" width="2560"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/0fq64w5rcuw89eliac.png"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/0bkdrfxo9otcwzk0ro.png"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/4bck1wi6m8hdnb2ta4.png"/></p></div><p style="text-align:right"><a href="https://jiajian233.xyz/notes/7#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/notes/7</link><guid isPermaLink="true">https://jiajian233.xyz/notes/7</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Mon, 08 Jun 2026 15:26:54 GMT</pubDate></item><item><title><![CDATA[嘉兴一日游]]></title><description><![CDATA[<link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/7awf6fxs8608qfc8rt.jpg"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/7c0fsn2zd20w0zv34f.jpg"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/f5633nhe6hhmoz7tqx.jpg"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/e7y7k3hrd1z25t83s0.jpg"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/1mqyvb1ibmh1xrxezd.jpg"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/0gy0dhsfsbqe81kupk.jpg"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/usl7hadxvts1s8ziq0.jpg"/><link rel="preload" as="image" href="https://jiajian233.xyz/api/v2/objects/image/gc2p4f8d4xqptf3wug.jpg"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/notes/6">https://jiajian233.xyz/notes/6</a></blockquote><div><p>和家里人在嘉兴南湖玩了一天</p><p><img height="3584" src="https://jiajian233.xyz/api/v2/objects/image/7awf6fxs8608qfc8rt.jpg" width="4096"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/7c0fsn2zd20w0zv34f.jpg"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/f5633nhe6hhmoz7tqx.jpg"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/e7y7k3hrd1z25t83s0.jpg"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/1mqyvb1ibmh1xrxezd.jpg"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/0gy0dhsfsbqe81kupk.jpg"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/usl7hadxvts1s8ziq0.jpg"/></p><p><img src="https://jiajian233.xyz/api/v2/objects/image/gc2p4f8d4xqptf3wug.jpg"/></p></div><p style="text-align:right"><a href="https://jiajian233.xyz/notes/6#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/notes/6</link><guid isPermaLink="true">https://jiajian233.xyz/notes/6</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Sun, 07 Jun 2026 12:57:41 GMT</pubDate></item><item><title><![CDATA[Astrbot 的 Hindsight 记忆插件]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/posts/project/astrbot-hindsight">https://jiajian233.xyz/posts/project/astrbot-hindsight</a></blockquote><div><p><a href="https://github.com/wjiajian/astrbot_plugin_hindsight_memory">https://github.com/wjiajian/astrbot_plugin_hindsight_memory</a></p><p>这是一个 AstrBot 插件，用于接入 Hindsight Cloud，为私聊和群聊提供按会话隔离的长期记忆能力。</p><p>Hindsight 是一个面向 AI 应用的长期记忆服务，可以把对话中的信息沉淀到 Memory Bank，并在后续请求中按语义检索相关记忆。Hindsight Cloud 提供托管版 API 和后台管理界面，适合在不自建向量数据库、不维护记忆管理 WebUI 的情况下，为 Bot 增加可持续积累和召回的记忆能力。</p><p>插件不修改 AstrBot 核心，也不提供自定义 WebUI。配置仍然通过 AstrBot 根据 <code>_conf_schema.json</code> 生成的插件配置界面完成，记忆管理使用 Hindsight Cloud 自带后台。</p><center><strong><em>姊妹插件</em></strong></center><p><a href="https://github.com/wjiajian/astrbot_plugin_supermemory">https://github.com/wjiajian/astrbot_plugin_supermemory</a></p><h2 id="">功能特性</h2><ul><li>在每次 LLM 请求前，从 Hindsight Cloud 召回相关记忆。</li><li>通过 <code>extra_user_content_parts</code> 注入临时 <code>&lt;hindsight_memory&gt;</code> 内容，不写入 AstrBot 持久会话历史。</li><li>在 LLM 回复后，按写入判定策略将值得长期保存的内容写入 Hindsight Cloud。</li><li>私聊和群聊严格按 scope 隔离，群聊使用“群公共记忆 + 群成员个人记忆”双层召回，避免同群不同用户的个人记忆互相串扰。</li><li>发送到 Hindsight 的 sender ID、group ID、<code>unified_msg_origin</code> 都会先做 hash。</li><li>可启用统一的 <code>memory_ai_*</code> 配置，让小模型负责判断是否写入、生成自然事实句，并为自动召回和 <code>/hindsight recall</code> 拓展搜索 query。</li><li>提供 <code>/hindsight</code> 命令，用于状态检查、手动召回和当前会话临时开关。</li></ul><h2 id="">平台兼容性</h2><p>已测试 AstrBot 平台：</p><ul><li><code>aiocqhttp</code></li><li><code>qq_official</code></li><li><code>qq_official_webhook</code></li></ul><p>其他 AstrBot 平台理论兼容，未逐一测试。</p><h2 id="">安装与配置</h2><ol start="1"><li>在 Hindsight Cloud 创建一个 Memory Bank。</li><li>为该 Bank 创建 Bank-scoped API Key。</li><li>将本仓库安装到 AstrBot 插件目录。</li><li>在 AstrBot 插件配置中填写：
<ul><li><code>api_key</code>：Hindsight Cloud API Key</li><li><code>bank_id</code>：Hindsight Memory Bank ID</li><li><code>api_base</code>：保持默认值 <code>https://api.hindsight.vectorize.io</code></li><li><code>recall_item_max_chars</code>：每条召回记忆注入前的最大字符数，默认 <code>360</code></li><li><code>memory_extract_max_depth</code>：解析召回结果嵌套结构的最大深度，默认 <code>4</code></li><li><code>memory_ai_enabled</code>：是否启用 AI 记忆分析和召回拓展，默认关闭</li><li><code>memory_ai_provider_id</code>：建议选择便宜、快速的小模型 Provider；留空时使用当前会话 Provider</li></ul></li><li>保存配置后，重载插件或重启 AstrBot。</li></ol><p>插件依赖只包含 <code>httpx</code>，AstrBot 通常会根据 <code>requirements.txt</code> 自动安装。</p><p>如果需要手动安装依赖，可在 AstrBot 环境中执行：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">pip install -r data/plugins/astrbot_plugin_hindsight_memory/requirements.txt
</code></pre>
<h2 id="">配置项说明</h2><ul><li><code>enabled</code>：全局启用或关闭插件。</li><li><code>api_base</code>：Hindsight Cloud API Base URL，默认 <code>https://api.hindsight.vectorize.io</code>。</li><li><code>api_key</code>：Hindsight Cloud API Key。</li><li><code>bank_id</code>：Hindsight Memory Bank ID。</li><li><code>enable_private_memory</code>：启用私聊记忆。</li><li><code>enable_group_memory</code>：启用群聊记忆。</li><li><code>recall_limit</code>：每轮最多注入的记忆条数。</li><li><code>recall_item_max_chars</code>：每条召回记忆注入前的最大字符数，默认 <code>360</code>。</li><li><code>memory_extract_max_depth</code>：解析召回结果嵌套结构的最大深度，默认 <code>4</code>。</li><li><code>retain_enabled</code>：启用 LLM 回复后写入 Hindsight。</li><li><code>retain_decision_mode</code>：写入判定模式，默认 <code>balanced</code>。<code>all</code> 保持旧行为，<code>balanced</code> 只自动写入较稳定的记忆，<code>strict</code> 只写入明确要求记住的内容。</li><li><code>retain_min_chars</code>：自动写入判定的最小有效字符数，默认 <code>8</code>。</li><li><code>retain_sensitive_requires_explicit</code>：邮箱、手机号、身份证等敏感信息必须明确要求记住才允许写入，默认开启；API Key、密码、token、private key 永不自动写入。</li><li><code>memory_ai_enabled</code>：启用 AI 记忆分析和召回拓展，默认关闭。开启后写入判定和召回 query 拓展都会额外调用 AstrBot LLM Provider。</li><li><code>memory_ai_provider_id</code>：AI 记忆分析使用的 AstrBot LLM 供应商。建议单独配置便宜、快速的小模型；留空时使用当前会话供应商。</li><li><code>memory_ai_fallback_to_current_provider</code>：所选 AI 记忆供应商调用失败时是否回退到当前会话供应商，默认开启。</li><li><code>memory_ai_min_confidence</code>：AI 写入判定最低置信度，默认 <code>0.7</code>。低于该值会跳过写入。</li><li><code>retain_ai_enabled</code> / <code>retain_ai_provider_id</code> / <code>retain_ai_fallback_to_current_provider</code> / <code>retain_ai_min_confidence</code>：兼容旧配置；新安装和新配置建议使用 <code>memory_ai_*</code>。</li><li><code>retain_dedupe_enabled</code>：写入前先检索当前 scope，跳过高度相似的重复记忆，默认开启。</li><li><code>retain_dedupe_threshold</code>：重复记忆相似度阈值，默认 <code>0.85</code>。</li><li><code>retain_dedupe_limit</code>：写入前去重最多检查的历史记忆条数，默认 <code>5</code>。</li><li><code>retain_write_raw_conversation</code>：写入原始本轮对话而不是精炼后的记忆文本，默认关闭；<code>retain_decision_mode=all</code> 始终保持原始对话写入。</li><li><code>retain_user_message</code>：写入用户本轮消息。</li><li><code>retain_assistant_message</code>：写入助手本轮回复。</li><li><code>request_timeout_seconds</code>：Hindsight 请求超时时间，单位秒。</li></ul><h2 id="">写入判定策略</h2><p>写入流程现在采用 AI 优先、规则兜底：</p><ol start="1"><li>如果 <code>memory_ai_enabled=true</code>，本地只做空文本、命令、硬敏感信息等必要过滤，然后直接调用 AI 判断是否写入、写入什么事实句、写入哪个 scope，以及记忆类型和置信度。</li><li>AI 必须返回结构化 JSON；插件解析失败、调用失败或置信度低于 <code>memory_ai_min_confidence</code> 时，不影响聊天流程，会回退到本地精简规则。</li><li>如果未启用 AI，本地规则只保留明确记忆意图、基础稳定事实、群规则/项目事实、个人偏好等几类，不再依赖大量手写锚点。</li><li>写入前仍会用 <code>memory_text</code> 在当前 scope recall，若相似度超过 <code>retain_dedupe_threshold</code> 则跳过；纠正类记忆会继续写入，并在 metadata 中标记为 <code>correction</code>。</li></ol><p>AI 生成的 <code>memory_text</code> 会作为自然、独立、可检索的事实句写入，不再强制添加人工锚点前缀。默认不会额外调用 LLM；只有开启 <code>memory_ai_enabled</code> 后才会调用小模型。需要兼容旧行为时，可设置 <code>retain_decision_mode=all</code>。</p><h2 id="">召回拓展策略</h2><p>默认召回只使用用户原始 query。开启 <code>memory_ai_enabled</code> 后，每次自动召回和手动 <code>/hindsight recall</code> 都会先让 AI 将原始 query 改写成 1 到 4 条搜索 query，插件会逐条检索、按记忆文本去重，并按 <code>recall_limit</code> 注入结果。AI 只负责拓展搜索词，不允许回答用户问题；如果 AI 调用失败或 JSON 解析失败，则直接回退为只使用原 query。</p><p>Hindsight 仍固定使用原有 recall API 和 <code>tags_match: all_strict</code>，本版不引入阈值配置。</p><h2 id="">命令</h2><ul><li><code>/hindsight status</code>：检查配置完整性和 Hindsight Cloud 连通性。</li><li><code>/hindsight recall &lt;query&gt;</code>：在当前会话 scope 下手动检索记忆。</li><li><code>/hindsight on</code>：启用当前会话记忆。</li><li><code>/hindsight off</code>：关闭当前会话记忆。</li><li><code>/hindsight help</code>：显示命令帮助。</li></ul><p><code>/hindsight on</code> 和 <code>/hindsight off</code> 的状态保存在 AstrBot 插件数据目录中，只影响当前会话 scope，不会修改 Hindsight Cloud 中已有的记忆。</p><h2 id="scope-">Scope 隔离策略</h2><p>私聊使用以下 tags：</p><pre class="language-text lang-text"><code class="language-text lang-text">scope:private
platform:&lt;platform_id&gt;
sender:&lt;sender_id_hash&gt;
umo:&lt;umo_hash&gt;
</code></pre>
<p>群聊使用双层 tags。</p><p>群公共记忆：</p><pre class="language-text lang-text"><code class="language-text lang-text">scope:group
scope:group_shared
platform:&lt;platform_id&gt;
group:&lt;group_id_hash&gt;
umo:&lt;umo_hash&gt;
</code></pre>
<p>群成员个人记忆：</p><pre class="language-text lang-text"><code class="language-text lang-text">scope:group
scope:group_member
platform:&lt;platform_id&gt;
group:&lt;group_id_hash&gt;
sender:&lt;sender_id_hash&gt;
umo:&lt;umo_hash&gt;
</code></pre>
<p>召回时固定使用 <code>tags_match: all_strict</code>。私聊只召回当前私聊 scope 的记忆；群聊会同时召回当前群的公共记忆和当前发言成员在该群内的个人记忆。启用 <code>memory_ai_enabled</code> 时由 AI 在允许范围内选择 <code>group_shared</code> 或 <code>group_member</code>；未启用 AI 或 AI 失败时，本地规则会把群规则、群公告、项目约定等内容写入群公共层，把个人偏好、称呼、个人资料等内容写入群成员个人层。<code>retain_decision_mode=all</code> 时保持旧行为，同时写入群公共层和群成员个人层。</p><p>写入 metadata 会附带 <code>retention_reason</code>、<code>retention_type</code>、<code>retention_source</code>、<code>retention_confidence</code> 和 <code>retention_action</code>，便于在 Hindsight Cloud 后台排查某条记忆来自规则、AI 判断还是纠正/补充写入。</p><h2 id="id-">ID 稳定性与迁移注意事项</h2><p>正常重启 AstrBot 通常不会改变记忆 scope。插件生成 tags 时会使用：</p><ul><li><code>platform_id</code>：AstrBot 平台名，例如 <code>aiocqhttp</code>、<code>qq_official</code>。</li><li><code>sender_id</code>：平台提供的用户 ID。</li><li><code>group_id</code>：平台提供的群 ID。</li><li><code>unified_msg_origin</code>：AstrBot 的会话来源标识。</li><li>本地 <code>salt</code>：插件首次运行时生成，并保存在 AstrBot 插件数据目录中。</li></ul><p>只要平台适配器、机器人账号和插件数据目录不变，重启后 hash 出来的 tags 应保持稳定，旧记忆可以继续召回。</p><p>以下情况可能导致同一个用户或群生成不同 scope，从而召回不到旧记忆：</p><ul><li>删除或迁移时丢失插件数据目录，导致 <code>salt.txt</code> 重新生成。</li><li>更换平台适配器，例如从 <code>aiocqhttp</code> 切换到 <code>qq_official</code>。</li><li>更换机器人账号、QQ 官方应用或平台配置，导致平台侧用户 ID 变化。</li><li>平台或 AstrBot 适配器更新后改变了 <code>sender_id</code>、<code>group_id</code>、<code>unified_msg_origin</code> 的生成方式。</li><li>群聊事件暂时拿不到 <code>group_id</code>，插件会降级为私聊 scope；之后如果又能拿到 <code>group_id</code>，scope 会发生变化。</li><li>群聊事件拿不到当前发言人的 <code>sender_id</code>，插件会跳过本轮记忆操作，避免多个未知用户被归入同一个个人记忆层。</li></ul><p>迁移 AstrBot 或插件时，建议同时备份插件数据目录中的 <code>salt.txt</code> 和 <code>scope_state.json</code>。其中 <code>salt.txt</code> 会影响历史记忆是否还能被同一 scope 召回，<code>scope_state.json</code> 保存 <code>/hindsight on</code> 和 <code>/hindsight off</code> 的当前会话开关状态。</p><h2 id="hindsight-api">Hindsight API</h2><p>插件直接调用 Hindsight Cloud REST API，不依赖官方 SDK：</p><ul><li>Recall：<code>POST /v1/default/banks/{bank_id}/memories/recall</code></li><li>Retain：<code>POST /v1/default/banks/{bank_id}/memories</code></li><li>状态检查：<code>GET /v1/default/banks/{bank_id}/tags?limit=1</code></li></ul><p>Retain 使用 item-level <code>tags</code>，并固定使用 <code>async: true</code>，减少对聊天流程延迟的影响。</p><h2 id="">测试方法</h2><h3 id="">本地单元测试</h3><p>在仓库根目录执行：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">python -B -m unittest discover -s tests -v
</code></pre>
<h3 id="">语法检查</h3><pre class="language-bash lang-bash"><code class="language-bash lang-bash">python -B -m py_compile main.py commands.py hindsight_client.py memory_formatter.py scope.py retention_policy.py memory_ai.py
</code></pre>
<h3 id="astrbot-">AstrBot 手动验收</h3><ol start="1"><li>在 AstrBot WebUI 中确认插件已启用，且 <code>api_key</code>、<code>bank_id</code> 已配置。</li><li><p>在聊天中发送：</p><pre class="language-text lang-text"><code class="language-text lang-text">/hindsight status
</code></pre>
<p>期望看到配置完整，并且 Hindsight Cloud 连接正常。</p></li><li><p>发送一条需要记忆的内容，例如：</p><pre class="language-text lang-text"><code class="language-text lang-text">我最喜欢的饮料是冰美式，请记住。
</code></pre>
</li><li><p>等待几秒到几十秒后，再问：</p><pre class="language-text lang-text"><code class="language-text lang-text">我最喜欢喝什么？
</code></pre>
<p>如果 Hindsight 已完成异步处理，模型应能通过 recall 回答出相关记忆。</p></li><li><p>测试当前会话开关：</p><pre class="language-text lang-text"><code class="language-text lang-text">/hindsight off
/hindsight on
</code></pre>
</li><li><p>在不同私聊、不同群聊之间分别测试，确认记忆不会跨 scope 召回。</p></li></ol></div><p style="text-align:right"><a href="https://jiajian233.xyz/posts/project/astrbot-hindsight#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/posts/project/astrbot-hindsight</link><guid isPermaLink="true">https://jiajian233.xyz/posts/project/astrbot-hindsight</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Fri, 05 Jun 2026 07:17:50 GMT</pubDate></item><item><title><![CDATA[照片墙怎么做呢]]></title><description><![CDATA[<p>往原址览之：<a href="https://jiajian233.xyz/notes/5">https://jiajian233.xyz/notes/5</a></p>]]></description><link>https://jiajian233.xyz/notes/5</link><guid isPermaLink="true">https://jiajian233.xyz/notes/5</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Thu, 04 Jun 2026 15:43:02 GMT</pubDate></item><item><title><![CDATA[关于先前的文章迁移]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/notes/4">https://jiajian233.xyz/notes/4</a></blockquote><span>之前的文章无法直接导入，应该是我之前用的头部元数据的问题，后续让 AI 整理一下自动导入吧。</span><p style="text-align:right"><a href="https://jiajian233.xyz/notes/4#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/notes/4</link><guid isPermaLink="true">https://jiajian233.xyz/notes/4</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Fri, 29 May 2026 08:12:15 GMT</pubDate></item><item><title><![CDATA[2025年度总结]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/posts/summery/2025">https://jiajian233.xyz/posts/summery/2025</a></blockquote><div><blockquote><p><em>以下为小学生流水账。</em></p></blockquote>
<h2 id="">独居生活</h2><p>2025年，从学生变成打工仔的一年。</p><p>一个人住在出租屋，连天然气都没有。入冬过后洗澡都不能洗太久，电热水器只能装一点热水。做饭还是用旧的电磁炉（虽然大多数情况下还是点外卖）。</p><h3 id="">我很佩服世界上第一个吃螃蟹的人</h3><p>跨年公司发的大闸蟹到手都没办法处理，下班现场买的蒸格（应该是叫这个吧，总之就是放在锅上蒸东西的那个东西），运气挺好买来和家里的锅刚好合得上。</p><p>跟着视频然后请教了一下网友，勉勉强强还是把大闸蟹蒸来吃了，沾点葱蒜生抽调料嗦着吃了。说是蟹心要去掉，但是我根本没看出来哪是蟹心，一口闷了算了。</p><p>总之……味道还不错，但也没感觉有多好吃，可能是做的不太好吧。</p><p>仔细算了一下，也有半年没自己正经做过饭了，不是外卖就是随便煮点面煮点抄手啥的。</p><hr/><h2 id="">五一出游</h2><p>上半年在重庆找工作，但是全要有经验的，基础太差技术没有，投了不知道多少简历，最后还是没有找到工作。</p><p>五一去了杭州和上海玩了玩。这边物价是真高啊，但是城市建设比重庆好多了。</p><h3 id="">杭州</h3><ul><li>和朋友转了转西湖</li><li>爬了爬灵隐寺</li><li>去杭电走了一圈</li></ul><h3 id="">上海</h3><p>本来是想去<strong>明日方舟音律联觉</strong>的，但是没买到票，就在上海瞎玩了五天：</p><ul><li>看了看明日方舟主题地铁站</li><li>跟着朋友走了一圈同济大学</li><li>跟朋友去看了看卖谷子的地方（忘了叫啥了都）</li><li>一个人逛了逛植物园</li><li>和看完音律联觉的朋友一起吃了饭</li></ul><p><strong>这还是第一次出去玩。</strong></p><hr/><h2 id="">工作</h2><p>尝试在这边找了找工作，最后在某电商企业做程序员了。
8：30-17：30，不加班但是大小周。
和互联网相比还是算比较轻松了。</p><hr/><h2 id="">待续</h2><p>不知道怎么写了，后面再补吧，图也后面再补。</p></div><p style="text-align:right"><a href="https://jiajian233.xyz/posts/summery/2025#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/posts/summery/2025</link><guid isPermaLink="true">https://jiajian233.xyz/posts/summery/2025</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Fri, 29 May 2026 07:41:43 GMT</pubDate></item><item><title><![CDATA[选择使用 Yohaku]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://jiajian233.xyz/notes/3">https://jiajian233.xyz/notes/3</a></blockquote><div><p>某一天在 GitHub 日 star 排行榜上看到了 lobehub 这个项目，项目链接：https://github.com/lobehub/lobehub</p><p>逛到核心开发者 Innei 的个人网站时一下子便被吸引住了，随即我便产生了制作个人网站的想法。由于我没有一点前端基础，只能借助 AI 去 Vibe Coding，磕磕绊绊做出了出来，当然在 Vibe 的过程中参考了很多 Yohaku（Shiro）的效果。</p><p>本来我是想实现前后端分离的，但是经验不足最后还是糊到了一起，用 Vue 做了一个静态网站 XD。最后实在做不下去了，还是直接用 Yohaku吧。</p></div><p style="text-align:right"><a href="https://jiajian233.xyz/notes/3#comments">览毕，何不一言？</a></p></div>]]></description><link>https://jiajian233.xyz/notes/3</link><guid isPermaLink="true">https://jiajian233.xyz/notes/3</guid><dc:creator><![CDATA[JiaJian]]></dc:creator><pubDate>Fri, 29 May 2026 07:30:33 GMT</pubDate></item></channel></rss>