交流
快来分享你的内容吧~
- 4 天前·Java后端华为OD机考介绍 我在9月21号中午首次与负责华为OD的HR(后续简称为对接人)沟通,9月22日开始刷题,准备参加9月27日的机考。考试这天我的运气还不错,能拿的分都拿到了,也是顺利地通过了机试这一关。 如果你打算应聘华为OD的相关岗位,机考是你的第一道关卡。只有机考通过后,你才有后续面试的资格。 机考一共三道题,需要在一坤时内完成。其中分值100分的题(基础题)两道,分值200分的题(压轴题)一查看全文lenyan:蹲后续~ 加油哥们我之前也投过,不过我学校不在名单里,恐怕要200+分😭之前力扣刷了400多道,半年没刷了就不想考了🤣474分享
- 09-23 17:34·Java后端
- 09-20 21:34
- 09-20 16:05
- 09-20 07:53·Java后端我以为一下午能写完的转码小工具,最后栽在三个「测试全绿」的缺陷上 我想做个东西,把一堆 mp4 的音频批量抽出来存成 MP3,音质能自己选。 按理说这是个下午茶的活。Python 生态里现成轮子一抓一大把,套个 tkinter 界面就完事,代码量撑死几百行。 然后这件事从头到尾都在打我的脸。不是因为它难,而是因为我几乎每一个「想当然」,最后都被证明是错的。 先摸底,第一脚就踩空 动手之前照例先看环查看全文加油鸭:太真实了!这种“测试全绿却踩坑到脚脖子”的转码实践,比教科书还扎实——你不是在写工具,是在用代码重写认知。致敬这份较真!231分享
- 09-16 17:09·后端开发大家好,我是不会喷火的小火龙。 最近半年,我的私信和评论区被同一类问题淹没了: "小火龙,我用 Cursor 10 分钟做了个 App,本地跑着没问题,一部署就各种报错,改了一下午越改越烂……" "AI 帮我写了个后台管理系统的 Demo,看着挺好的,我加了两个需求之后整个项目就崩了,连回退都回退不动。" 这些问题指向同一个根源。今天我把这件事说透。 一、喝着咖啡看 AI 刷屏很爽,直到我第一次尝查看全文加油鸭:太棒了!这篇深度复盘既有真实踩坑,又有可落地的防翻车铁律,干货密度拉满,真正帮开发者把AI用稳、用对!14314分享
分享工作一年半的感受
民办本毕业,青岛,25 年初跟着b 站大学,把苍穹外卖敲了一遍,从此进入 web 开发领域 ,对计算机就业产生了深厚兴趣,乐此不疲的学习,刚学了 3 个月,趁着校招,进了一家小外企性质的公司,赶上 AI 崛起,开始做 AI 智能客服。 起初并没有 Agent,vibe coding 这些在 AI 领域的词汇 ,那时候还是通过手敲代码,CSDN 搜索问题,遇到问题也不会翻q,只能去找文心一言,通义千问,也在 idea 中使用过 清华大学开源的一款代码补全工具,叫 code ... 什么来着,忘记了。 古法编程虽然累,但是真的快乐,通过不断调试,打开 browser F12 , 追踪 idea stuck ,不断的debug ,最后确定在一个','是中文 。 真的能被自己整笑。 还记得第一次开发 AI SSE , 只学过基本的 servlet ,不知道还有 SSE 这种协议, 也不懂什么是流式输出,吐字卡住了还调试半天,最终排查出来是 nginx 有一个默认的缓冲池代理,关掉之后流式输出就不卡顿了。 还为了模仿打字机效果,自己在前端构造 buffer,模拟流式输出,现在回头看,是属于自己的傻事。 薪资也从毕业的 3k ,一路5k , 5k , 7k ,到现在能达到9k ,我不聪明,全是背后默默‘卷’的结果,上班到现在,几乎每个假期都不敢放肆休息,总觉得少学一点就会 out , 毕竟 AI 时代的 noun 如此多。 从死磕springBoot 到现在 Go , python 百花齐放,能看懂 AI 写的,自己下笔却没有思路 。 以前死磕一门语言就能提薪的时代,在如今再也不是护城河 ,让我明白了,世界上没有一成不变的技术,不变的永远是在变。 学好英语,认真打好基础,会用并发,会分布式部署,线上日志追踪,删除操作前先备份,工作留痕 ,坚持输入 new thing ,坚持输出 work note 利己利人 。 毕竟 代码写不下去了,凭着英语好也能去外企混一混 。
26.9.27 华为OD机考经验分享
# 华为OD机考介绍 > 我在9月21号中午首次与负责华为OD的HR(后续简称为对接人)沟通,9月22日开始刷题,准备参加9月27日的机考。考试这天我的运气还不错,能拿的分都拿到了,也是顺利地通过了机试这一关。 如果你打算应聘华为OD的相关岗位,机考是你的第一道关卡。只有机考通过后,你才有后续面试的资格。 机考一共三道题,其中分值100分的题(基础题)两道,分值200分的题(压轴题)一道。考试时间一般为每周的周三、周日的晚上8点开始,一共考一坤时。 题目的计分方式为:题目分数 x 通过测试用例百分比。测试用例分布是有规律的,从易到难。假如其中你某个用例没通过,那么当前用例后续的所有用例将不会被执行。 关于及格线,网上众说风云,具体还需要咨询你的对接人。假如你的学校是9/2/双一流院校或者在华为OD目标院校名单内的双非院校(网上所谓的名单可能已经过时了,具体请咨询你的对接人),及格线一般都是150分。机考分数是你能否进入面试环节的敲门砖,最后还要综合你的面试表现进行定级。 从2026年4月份开始,OD机考的考试形式从传统的 ACM 模式正式切换为和 LeetCode 一样的核心代码模式,我个人觉得这是好事,毕竟不用再和标准输入输出斗智斗勇了,可以把注意力放在核心逻辑的实现上。 官方的考试客户端为牛客,平时可以在牛客上面练习,熟悉OD机考在线编码环境的风格。考试时,允许使用本地 IDE (需要禁用所有 AI 相关插件)。 我的对接人特别强调过:**机考的时候用什么语言,面试和未来工作就要用什么语言**。请结合你的实际情况选择合适的编程语言。我认为,编程语言内置的工具越多,刷题时也会更得心应手一些。比如我用的 Java,我在刷题过程中就大量运用了 Java 的以下能力: - Stream API - 集合框架:Map、List、Set、Deque... - Comparator - 日期时间 API - 字符串处理 API、StringBuilder、StringJoiner、简单正则表达式... - Math 、Arrays 工具类... - ... 它们也是帮我很方便地解决了不少题目。 # 关于题库 你的对接人会给你往期的真题题库进行练习。 我的对接人给了我两种题库: 第一种是 CSDN 专栏([华为OD机试 新系统 双机位C卷 真题题库目录|机考题库 + 算法考点详解_华为od机考2025b卷题目-CSDN博客](https://blog.csdn.net/qq_45776114/article/details/145076776))。 这个专栏本身是收费的,好在我的对接人帮我登录了她的账号,非常感谢我的对接人!  这个 CSDN 专栏有个缺点,就是所有题目的代码都是以 ACM 模式提供的,包括新系统之后的新题。  好在新系统题目的题面是完整保留的。 第二种题库是模拟考题库(https://moyiluo.com/)。 此网站需要邀请码才能使用,这需要咨询你的对接人获取。  此题库相较于 CSDN 题库有一个显著优势,即所有题目都是核心代码模式:  但它也有个缺点:不少题目的题面描述不清晰。 我的做法是结合使用这两个题库,在 CSDN 题库上看题面,在这个题库上提交代码。 需要注意的是,同一个题目在两个题库的标题不一定是一致的。我在练习的过程中有个好习惯就是顺手记录一个题目在两个题库上的标题,方便后续对照:  # 关于备考 我感觉现在的机考**每一次都是新题**,妄想通过背题库的方式通过OD机考的路子是走不通的! 首先,**你应该基于实际情况确定准备范围**。以我为例,由于我的及格分数线是150分,所以我只需要把两个100分的基础题稳稳拿下就够了,最后一道200分的压轴题就尽我所能,“骗”点分数。所以我的准备范围就重点落在100分的题目上,200分的题由于水平还达不到只能先行放弃。 然后,**你应该按照题型系统性学习**。同一个题型下的问题做多了后,你应该会慢慢领悟这个题型的通用套路。 第二个题库支持按题型筛选题目:  我自己按照题型整理了一个核心题题型分布,然后基于我的实际情况进行从易到难的排序:  > 当然,由于时间紧迫,我没能刷完所有题目。 最后就按照顺序刷题,在刷题过程中逐渐学习这个题型的解题思路。 在做题过程中,请不要过度依赖题库自带的题解,多去和 AI 对线。我自己几乎不看这些题解,因为不少题解过于跳跃和抽象(类似于一个数学大神跟基础薄弱的你说:"注意到..."),对我而言几乎没有任何实质性帮助,我顶多只是当作参考。 我在 ChatGPT 的帮助下写出了三数之和这道题,你可以参考我和它的对话过程:https://chatgpt.com/share/6ab9a164-b1bc-83ee-8b02-0fd06b105719
AI Coding 已经改变开发方式了,程序员到底还该学什么?
# 关于 AI 应用和现在程序员就业的一些看法 前段时间有朋友问我: **“现在 AI 这么火,要不要从 Java 转 AI 应用?”** 我当时第一反应是,这两个东西其实就不应该放在一起比较。 有点像几年前问: **“我要不要从 Java 转电商?”** AI 应用是一个方向,Java 是后端的一门语言,本身就不是一个维度的东西。 借这个问题,说一下我目前对 AI 应用、技术栈和程序员就业的一些看法。 都是这两年工作下来的一些个人感受,略带主观。 --- ### 1. 单一技术栈已经不太够用了 国内 Java 的存量盘依旧很大,这个短时间内不会有什么变化。 但我觉得现在只会 Java,或者把自己完全定义成一个 Java 工程师,已经不太够了。 目前后端我比较建议接触的还是: **Java、Go、Python。** Java 主要还是国内大量存量项目、企业级应用和现有团队技术栈。以后很多传统业务做 AI 赋能,也不可能把原来的东西全部推倒重来。 Go 在高并发、云原生这些场景本身就有优势。现在很多 AI 应用又恰好涉及大量并发、流式输出和长连接,用 Go 也比较合适。 Python 就不用说了。 AI 应用继续往下走,不管是 PyTorch、训练、推理还是 AI Infra,基本都绕不开。 当然我不是说三门语言都要学到一样深。 **至少有一门是自己的主语言,真正熟练,其他的能用、能看、能改就行。** 现在有 AI 以后,第二门、第三门语言的学习成本其实已经低很多了。 前端也是一样。 以前如果有人问我国内学 Vue 还是 React,我可能会更建议 Vue。 现在我会更建议 **React + TypeScript**。 主要还是国外生态更大,而且现在 AI Coding 对 React/TS 的支持也更好。 当然结合国内就业,Vue 最好还是得会。 所以我现在对技术栈的看法比较简单: **主技术栈一定要有,但没必要再给自己贴死某一门语言的标签。** --- ### 2. 一定要高强度使用 AI 这个是我目前最确定的一点。 AI Coding 已经不是“以后会不会普及”的问题了。 已经发生了。 我们目前甚至有一个真实的企业项目,基本就是纯 Vibe Coding 驱动开发。 而且不是 Demo。 项目本身是一个比较复杂的 AI 应用平台,涉及多租户、异步任务、分布式并发控制、实时通信、向量检索、全文检索、消息网关、服务监控和 CI/CD 等。 AI 这块也不只是调一下大模型 API,还涉及 RAG、多模态、ASR、视觉检索,以及 PyTorch、CUDA 相关的推理服务。 并且整个架构从一开始就是按照服务拆分、异步化和后续横向扩展去设计的。 说这些不是想证明这个项目有多复杂。 主要是想说,**这是一个有真实工程复杂度、需要长期维护的企业项目,而且大量代码确实是 AI 完成的。** 到目前为止,也没有出现所谓“Vibe Coding 到最后完全没法维护”的情况。 所以我现在觉得: **Vibe Coding 本身不是问题,问题是使用 AI 的人有没有工程能力。** 前两天我负责对接一个渠道商做兼容性的 RAD 开发。 方案、编码、测试基本全部由 AI 完成。 大概半天。 这个事情如果按照以前的开发方式,我估计至少两三周。 所以对于 AI,我没什么“要不要用”的建议。 **一定要用,而且要高强度地用。** 方案、编码、测试、Debug、Review、CI/CD,能用就用。 这已经是生产力问题了。 再说句题外话。 我现在觉得 AI 已经不只是开发工具了。 生活里很多需要搜索信息、学习、分析、做决策的事情,我都会先问一下 AI。 不一定听它的,但至少多一个信息源,多一个思考角度。 开发就更不用说了。 现在越来越多公司已经把 AI Coding 当成正常的研发工具,AI 带来的提效也是实打实的。 所以如果你长期处在一些接触不到 AI 的开发环境里,比如部分外包、纯内网、对日或者 ToG 项目,我觉得需要注意一个问题: **你损失的不只是 AI 帮你写代码的那点效率,而是在慢慢错过一套新的研发方式。** 短期当然没什么。 但如果三年、五年一直处在这种环境里,而外面的工程师已经习惯用 AI 做方案、编码、测试、Review、Debug,甚至完成整个研发流程,那两边积累下来的工作方式和效率差距会越来越大。 所以如果工作环境确实用不了 AI,我反而更建议自己在工作之外保持使用。 **可以不用 AI 写公司的代码,但不要让自己长期脱离 AI。** 这也是为什么我一直强调高强度使用 AI。 不是因为现在 AI 火。 而是我觉得,**会不会把 AI 真正融入自己的工作流,很可能会逐渐变成工程师的基础能力。** --- ### 3. AI 越强,基本功反而越重要 这一点我最近感受也比较深。 24 年甚至更早的时候,我们是真的会为了一个环境折腾半天。 碰到一些难调的 Bug,就自己看日志、翻源码、抓包、排调用链,一点一点 Debug。 现在很多问题直接扔给 AI,可能几分钟就解决了。 所以有时候我反而觉得,26 年才开始学编程的人可能更难。 不是因为工具差,恰恰是因为**工具太好了。** 很多以前必须自己经历的过程,现在可以直接跳过去。但那些过程其实会慢慢形成一些工程直觉。 所以如果你是新人,问我要不要打好基础,我的答案是: **非常有必要。** 数据结构、操作系统这些基础该学还是得学。 同时至少选一门自己的主开发语言,真正学到熟练。不是会写几个接口、会调几个框架就叫熟练,而是真的拿它做过项目、调过问题,对它的运行机制和生态有足够的理解。 因为 AI 可以写代码,也可以 Debug。 **但 AI 给你的东西到底对不对,最后还是需要你判断。** 事务、并发、幂等、异常、性能、可观测性,包括系统出了问题应该从哪里开始查,这些东西不会因为 AI 出现就消失。 反而是 AI 把编码本身变得越来越便宜以后,**这些东西才是真正能拉开工程师深度的地方。** 以前没有 AI 的时候,大家卷框架、卷 API、卷谁记得多,我觉得没什么问题,因为当时这些东西确实直接影响开发效率。 但现在很多框架层面的东西,AI 比我们记得更全,写得也更快。 所以我现在反而更建议新人把时间往基础上放一点。 当然,这个建议也不只是给新人。 **包括我自己,现在也在重新花更多精力,更专业、更体系化地去学这些基础。** 以前很多东西可能是工作里遇到了再学,或者知道怎么用就够了。但现在有了 AI,我反而觉得可以把以前花在记框架、记 API 上的时间拿回来,真正去理解一些更底层、更长期的东西。 有几本书我一直比较推荐: 1. Clean Code 2. The Clean Coder 3. Understanding the Linux Kernel 4. Head First Design Patterns 5. Designing Data-Intensive Applications 当然,书只是一个载体。 我真正想表达的是: **AI 时代不是不需要基本功了,而是以前很多“会用框架”的价值正在被 AI 吃掉,基础和工程判断的价值反而更高了。** AI 可以替你少走很多弯路,但有些路你最好真的走过。 所以我现在越来越觉得,AI 真正放大的不是编码能力。 **是判断能力。** --- ### 4. AI 应用可以转,但现在已经不是做几个 Demo 就好找工作的阶段了 我自己其实算比较早转 AI 应用的。 **25 年 10 月,我就从传统开发转到 AI 赛道了。** 所以如果提前半年或者一年有人问我: “会 RAG、做几个 Agent 项目,能不能转 AI 应用?” 我会觉得问题不大。 但现在我的看法已经变了。 不是 AI 应用不值得做,恰恰是因为**这个方向太火了,进来的人太多了。** 现在很多简历翻来覆去都是: RAG 知识库、AI 客服、PDF 问答、LangChain、Embedding、Milvus、Rerank…… 这些东西当然要会,我自己也在做。 但问题是,**会这些东西的人已经太多了。** 更现实一点说,现在如果只是拿几个这种项目去找 AI 应用的工作,我觉得已经没有以前那么容易了。 而且关于项目这件事,我可以说得直接一点: **现在国内网上能看到的大量所谓 AI 应用项目,我觉得本质上还是玩具项目。** 换一个模型、接一个知识库、套一个 Agent 框架,最后做个聊天页面。 能跑。 但离真正的企业项目还很远。 所以如果现在还准备转 AI 应用,我更建议去看: **真实业务、真实客户、真实场景。** 我前公司是做 AI + 教育的。 国内这一块真正落地的东西,无非也是 AI 答疑、AI 批改、OCR/VLM、个性化学习这些。 AI 答疑底层一样可能是 RAG。 区别只是它最后真的要面对学生、老师和学校,真的要解决问题。 所以不是不要学 RAG,也不是不要学 Agent。 **这些东西该学还是得学,只是别再把“会 RAG、会调 Agent”本身当成竞争力了。** --- ### 5. 除了业务深度,还需要一点技术纵深 上面说的是业务。 但只解决“项目别做成 Demo”这一个问题,我觉得还不够。 现在 AI Application / Agent 这个方向已经很热了,而且这个趋势越来越明显。 当大量工程师都开始做 Agent、RAG、工作流、MCP 以后,应用层的技术差异一定会越来越小。 所以除了去做真实业务以外,我现在还有另外一个建议: **适当往下走,给自己增加一点技术纵深。** 这也是我自己接下来准备深入 AI Infra 的原因。 我目前比较看好: **后端 / 分布式 + AI Application / Agent + AI Infra。** 不是放弃应用层。 恰恰相反,我觉得上层的业务经验很重要,只是在这个基础上继续往模型推理和基础设施走一点。 当然,AI Infra 也别最后学成下一个 RAG。 不是会 Docker、K8s、vLLM,再部署一下 CUDA 环境,就叫 AI Infra。 继续往下还有 Inference Serving、Batching、Scheduling、KV Cache、GPU Memory、PyTorch、CUDA、NCCL、GPU Cluster,以及监控、扩缩容、容错、成本这些东西。 这一块我自己也还在继续学,所以不展开,也不装懂。 我现在的想法其实很简单: **应用层越来越拥挤,就不要只在应用层继续横向堆东西。** 业务上往真实场景走,技术上往深处走。 至少目前,我自己准备这么走。 --- 最后其实就两个建议。 **1. 高强度使用 AI,别和生产力过不去。** 不要只拿 AI 补两行代码。 方案、编码、测试、Debug、Review、CI/CD,能用就用。 **2. 如果准备转 AI,别再堆玩具 Demo。** 去看真实业务,解决真实问题,再给自己找一个方向往深了走。 至于 Java、Go、Python、React、Vue,该用什么就用什么。 **主技术栈要有,工程基本功别丢,AI 狠狠用。** 差不多就这些。 听不听随意。
低 sims 下棋类游戏强化学习如何逃出双盲期
# 低 sims 下棋类游戏强化学习如何逃出双盲期 > 本文以一个五子棋 CNN 自对弈项目的真实调参经历为案例。 > 项目采用 30 sims 低算力管线(参考项目: <https://github.com/zhoukangyang233/Gomoku>),开局就撞上典型的"双盲期": > 模型不会防守、对局极短、胜率看似暴涨实则空转——这个均衡持续了 > 约 1200 轮才被打破,随后约百轮模型达到中等 AI 强度 > (完整实测时间线见 §4.7)。 > 本文记录双盲期的定义、成因、诊断指标,以及逃出它的全部已知手段。 *** ## 1. 什么是双盲期 **定义**:自对弈强化学习的冷启动阶段——对局双方(同一模型的自我复制) 策略都弱到数据分布中**不含任何有意义的对抗行为**(防守、战术、终局 经营),模型从这样的数据里学不到进步方向,形成自我强化的低水平均衡。 双盲期的"双"指对局双方同时失明: ``` 模型弱 → 搜索树里没有战术信号(双方都不堵、不攻) → 产生的训练数据里没有"防守收益"的正例 → 模型学不会防守 → 模型依然弱 ``` ### 现象特征(run\_1 双盲期实测) | 特征 | 实测值 | 说明 | | ------------------ | ----------------------------------------------------- | ------------------------------ | | 对局极短 | 自对弈 \~3.5 s/100 局(C++ MCTS + 4 worker),折合每局仅 \~10 手搜索 | 双方都不堵,谁先随手连五谁赢 | | 胜率暴涨假象 | arena 对随机 base 99:1 | 对手是纯随机的 base 模型,赢它不需要会下棋 | | value 在学、policy 原地 | val value 0.088→0.029,val policy 0.088→0.14(恶化) | 终局胜负信号先教会 value 头;policy 无信号可学 | | 人机试玩不会防守 | 玩家做活三,AI 视而不见 | policy 头不认为堵截重要 | 关键区分:**双盲期 ≠ 训练失败**。value 头在进步(终局 z 信号永远存在), 它是逃逸的种子;但 policy 头的行为改进要等数据分布先改变——这就是 为什么双盲期只能"熬 + 疏导",不能"调一两个参数立刻解决"。 run\_1 的双盲均衡实际延续到 r1100 前后才被打破(§4.7 时间线), "熬"的时长要有心理预期。 *** ## 2. 明知有双盲期,为什么还选低 sims ### 2.1 算力经济学:单轮标签精度 vs 总轮次 自对弈训练的棋力来源可以分解为: ``` 总学习信号量 ≈ 总轮次 × 每轮数据量 × 每样本信息密度 ``` 高 sims 与低 sims 在训练**前期**生成的数据质量差别很大:前者 预算充足,访问分布和树内 Q 都有分辨力,即使处于双盲期,标签里 也有可学的东西;后者每个子节点只摊到寥寥几次访问,前期产生的 标签基本是噪声。低 sims 方案的全部设计,就是在接受这个前期 数据质量劣势的前提下,用标签生成机制补回信息密度(§4.5), 用总轮次补回学习信号总量。 ### 2.2 标签质量不来自 sims 数,而来自标签生成机制 低 sims 的核心赌注:**标签质量靠"层次重分配"机制保证,而不是靠堆 sims**。30 sims 的原始访问分布确实是噪声(每个子节点 1-3 次访问, 分辨不出好坏),但层次重分配把树内 Q 的对比信号注入 policy 标签, 让低 visits 数据变得可用(详见 §4.5)。 ### 2.3 三支柱缺一不可 参考实现的配方是三件事绑定: 1. **30 sims**(低算力 → 高轮次) 2. **全节点收集 + 层次重分配标签**(低 visits 数据可用) 3. **小网络 32ch/5blk**(容量匹配噪声标签;128ch 大网络会在标签 噪声上过拟合,且训练段计算量 19 倍于小网络) 只搬其中两根支柱(例如用大网络配 30 sims)会得到最差组合:本项目 低 sims 运行首次启动时单轮 8 分钟(大网络吃不动 28 万全收样本), 改成 32ch 后实测全程平均约 22 秒/轮(自对弈 \~6 秒 + 训练 \~15 秒), 1380 轮共 8.4 小时,逃逸点 r1200 折合不到 8 小时算力。 ### 2.4 代价要有预期 低 sims 的代价就是**双盲期更长**:搜索看不到战术序列(30 sims 树深 \~3 层且每节点 1-3 visits 无分辨力),战术能力只能等网络从数据里 "背会"棋形——这需要海量对局。这是设计内风险,不是 bug。 *** ## 3. 双盲期的量化诊断 不要靠肉眼感觉"模型好弱",用这三个指标: ### 3.1 对局长度(最灵敏的涌现指标) 防守涌现 = 双方开始互堵 = 对局拉锯变长。**自对弈耗时/轮会随防守 涌现而变长**——耗时上升不是坏事,是行为改变的信号。 run\_1 实测(前置条件:**C++ MCTS 后端** + 4 worker;换纯 Python 后端 单步耗时会成倍变慢,以下绝对数值不可直接对比):双盲期自对弈 \~3.3-3.6 s/100 局,逃逸后台阶式上升到稳定 \~7-8 s/100 局不再回落 (对局从随手连五变成互堵拉锯)。注意 r400-600 也出现过一次耗时冲高(\~9.6 s)随后回落——平台期的单次 冲高不算数,**台阶式上升且不回落**才是涌现。 ### 3.2 arena 对 base 的胜率曲线 对随机 base 99% 是假进步(随机对手不设防)。有意义的信号是 **对 champion 的胜率能否持续区分**:如果每次 arena 都在 50-60% 震荡且晋升不断,说明模型在代际进步;连续多轮 45-50% 停滞则警惕。 ### 3.3 value/policy 的分离演化 双盲期标志性形态:**value loss 下降 + policy loss 停滞或恶化**。 value 头从终局信号学(有监督),policy 头从访问分布学(无信号)。 这条分离曲线是双盲期的指纹——value 降得动说明管线正常。 但run\_1 有个意外发现:**val policy loss 全程没有"掉头向下"** (0.088 起步,r1379 反而 0.12),模型照样逃逸了。原因:层次重分配 标签是**移动靶**——value 头变准 → 树内 Q 变准 → policy 标签分布 本身在变锐,KL 对一个越来越难的目标不降反升。所以低 sims + 重分配 管线里,policy loss 不能当逃逸判据;逃逸确认要靠 §3.1 的耗时台阶、 §3.2 的 arena 持续区分和人机实测。 *** ## 4. 逃出双盲期的条件 以下手段按"重要性 × 容易被忽视"排序。前三个是数据多样性来源, 后三个是标签信号来源。 ### 4.1 随机开局——数据覆盖的中盘注入 纯空盘自对弈的问题:开局访问分布几乎纯先验(网络没见过空盘), 30 sims 下 policy 标签退化成"先验 top-k 摊平",且所有对局开局段 高度相似(数据冗余)。随机预落子把对局起点直接推进到中盘,局面 结构丰富,重分配标签的信息密度高得多。 两个关键参数: ```python # 配置文件 opening_max_moves = 20 # 预落子手数上限(均匀随机 0~20) opening_empty_prob = 0.3 # 30% 概率纯空盘开局(保开局覆盖) ``` 生成方式要点:**每个候选开局独立采样 + 用网络估值筛选"最均势" 局面**(避免一开局就是必胜/必败局,那种对局的 value 标签没有 区分度): ```python # 工具模块(节选,真实实现) def generate_opening_boards(model, num_boards, ...): """随机预落子生成开局候选,批量估值后挑最均势的。""" candidates = [] for _ in range(num_boards): # 每个候选独立采样—— board = board_mod.new_board() # 千万不要共享同一个排列! num_moves = random.randint(0, Config.opening_max_moves) empty = [(i, j) for i in range(bs) for j in range(bs)] moves = random.sample(empty, num_moves) color = 1 for r, c in moves: board[r][c] = color color = -color candidates.append(board) # 批量估值,挑 |value| 最小(最均势)的且未终局的 values = evaluate_batch(model, candidates) picked = [b for b, v in zip(candidates, values) if evaluation_status(b) == 0] # 跳过已终局局面 picked.sort(key=lambda b: abs(value_of(b))) # 越均势越靠前 return picked[:num_boards] ``` 反面教材(真实踩坑):参考实现的开局生成把 shuffle 放在循环外、 所有候选共享同一排列前缀——1000 个候选实际只有 \~11 种不同局面。 它靠无数轮硬堆掩盖了这个 bug,新实现不要模仿。 ### 4.2 温度调度——前期的行为多样性 温度控制落子采样对访问分布的"锐化"程度。双盲期的访问分布本身 就是摊平的(低 sims 无分辨力),高温采样 = 按先验近似均匀探索, 让"更好的一手"有机会被随机执行并赢下对局——数据里才会出现 进步的正例。 ```python # 配置文件 temp_high_moves = 12 # 前 12 手高温(开局+早中盘探索) temp_high = 1.0 # 按访问分布比例采样 temp_low = 0.2 # 之后低温(接近贪心,保证对局质量) ``` ```python # 落子采样:温度 0 = 贪心;温度 >0 = 按 probs^(1/T) 重加权采样 def calc_next_move(board, probs, temperature=0): valid = [(i, j, probs[i][j]) for i in range(bs) for j in range(bs) if board[i][j] == 0] if temperature == 0: return max(valid, key=lambda x: x[2])[:2] moves = [(i, j) for i, j, _ in valid] p = np.array([p for _, _, p in valid], dtype=np.float64) p = p ** (1.0 / temperature) p = p / p.sum() return moves[np.random.choice(len(moves), p=p)] ``` 参考实现用另一种思路——**每局随机温度**(0\~0.8 均匀),让整个 数据集覆盖从贪心到乱下的行为谱。两种都行,固定分段更可控, 逐局随机更多样。 ### 4.3 根节点 Dirichlet 噪声——搜索内的先验探索 MCTS 根节点的先验混入 Dirichlet 噪声,保证低 visits 阶段搜索 不会完全被先验锁死: ```python # 配置文件 dirichlet_alpha = 0.1 # α 越小噪声越集中(少数着法拿到大噪声) noise_eps = 0.25 # 根先验 = 0.75*prior + 0.25*noise ``` ```python # MCTS 根节点初始化(概念实现) noise = np.random.dirichlet([alpha] * len(children)) for i, child in enumerate(children): child.prior = (1 - eps) * child.prior + eps * noise[i] ``` 参考实现用了更彻底的思路——**每个展开节点**的先验都叠加小幅 高斯噪声(归一化 policy + N(0, 0.01) 抖动),而非只在根节点注入。 两者目的一致:防止搜索被先验锁死。根节点方案更干净(树内先验 与 Q 不被扰动),全节点方案探索更散、实现更简单。 低 sims 下的特殊注意:30 sims 摊在 20 个子节点上,每个 1-3 visits, **visits 噪声淹没 prior 差异**——高温采样的行为近似按先验随机。 这不是缺陷而是特性:它保证了双盲期的行为多样性。真正的问题是 §4.5 要解决的"标签信号从哪来"。 ### 4.4 PUCT 探索常数 `c_puct` 控制"先验偏好 vs 访问价值"的平衡。双盲期 Q 全是噪声, PUCT 退化为按先验分配 visits——这是正常的,不要在双盲期去调 c\_puct(没有任何可靠信号告诉你怎么调)。等 arena 开始有区分度 后再考虑微调。 ```python # 节点选择:PUCT = Q + c_puct * prior * sqrt(父visits) / (1 + visits) score = child.q + c_puct * child.prior * math.sqrt(parent.visits) / (1 + child.visits) ``` ### 4.5 层次重分配标签——低 sims 可行的核心机制 这是整个低 sims 方案的支点。30 sims 的原始访问分布是噪声,直接 当 policy 标签等于教模型学噪声。层次重分配的做法:**用子节点的 Q 值对比纠正先验的盲区,把访问概率重新分配给"树认为更好"的 分支**。 ```python # 层次重分配(节选简化) def reassign_policy(mcts_root_state, prior, ...): """prior + Q 层次重分配:纠正 prior 盲区。 原始访问分布 visits=1~3 无分辨力;但每个子节点下方的树内 Q 是连续信号——"这个分支背后藏着好局面"能被 Q 读出来。 """ children = mcts_root_state.children if not children: return prior # 1. 每个孩子的"修正分数" = Q 归一化后与先验混合 q = np.array([children[i].w_sum / max(children[i].visits, 1) for i in range(len(children))]) q = (q - q.min()) / (q.ptp() + 1e-8) # 归一到 [0,1] pri = prior[valid_moves].copy() pri = pri / (pri.sum() + 1e-8) # 2. 重分配:Q 高的分支吸收 Q 低分支的部分概率 weight = 0.5 * pri + 0.5 * q * pri / (pri.mean() + 1e-8) ... return new_policy ``` 配合全节点收集 + 相对权重,低 visits 节点不用丢弃——用权重 表达"这个节点有多可信": ```python # 节点收集:visits 门槛设 0(全收),可信度进权重 weight = math.sqrt(visits / num_sims) # visits=1/30 → 0.18 # value = 树内 Q(内部节点自举),终局节点用真实胜负 z ``` **双盲期里重分配的 Q 也全是噪声**——它救不了双盲期本身。它的 价值在于:一旦 value 头开始从终局信号学到东西,Q 变准的瞬间 重分配标签立刻变准,**policy 头不需要等数据分布先改变**。这是 把逃逸正反馈链的长度砍半的关键设计。 ### 4.6 value 头:逃逸的正反馈引擎 双盲期唯一可靠的监督信号是**终局胜负 z**——对局总会结束,胜负 永远真实。逃逸链条: ``` 终局 z → value 头学会"哪些局面会输" → 树内 Q 变准 → 重分配 policy 标签变准 → policy 头学会堵活三(先从 Q 读出来的) → 对局开始互堵、拉长 → 数据里出现真正的战术序列 → policy 直接从访问分布也能学 → 正循环 ``` 诊断双盲期时盯住 val value loss:它在降说明引擎在转。value 先于 policy 改善是正常时序(低 sims 运行实测:val value 0.088→0.029 而 policy 原地)。但"引擎在转"不等于"马上逃逸"——run\_1 value 头降了上千 轮才等来逃逸。 ### 4.7 耐心与数据量门槛(附完整逃逸时间线) 防守是模式识别问题:32ch CNN 需要"见过足够多活三/冲四被惩罚" 的对局才能形成形状记忆。run\_1 逃逸时累计约 12 万局(r1200 × 100 局/轮),与参考实现的 20 万局同一数量级——门槛真实存在, 管线优化只是把它压低,没有取消它。 **run\_1 实测时间线**(每轮 100 局,单轮 \~22 秒;C++ MCTS 后端 + 4 worker): | 阶段 | 轮次 | 自弈耗时/轮 | arena 表现 | 状态 | | ---- | ----------- | ------------------ | --------------------- | ------------ | | 双盲期 | r0\~99 | \~3.6 s | 频繁晋升(对手是弱 base,假进步) | 对局极短,随手连五 | | 长平台 | r100\~799 | 3.6→9.6→5.5 s 冲高回落 | 晋升频繁但幅度小 | 行为在变,战术未成型 | | 深平台 | r800\~1049 | \~3.4 s | 胜率 50% 无区分,两次 LR 跳回 | 全程最像"失败"的阶段 | | 逃逸启动 | r1050\~1149 | 3.5→5.0 s | 第三次跳回后场均 82.5%,连续大胜晋升 | 突破 | | 巩固 | r1150\~1379 | 稳定 7-8 s | 持续 54-63% 震荡晋升 | 达到中等 AI 强度 | 三点教训: 1. **深平台段(r800\~1049,胜率 50% 无区分 + 耗时回落到双盲水平) 是全程最像"该止损"的阶段——但它恰恰是逃逸前夜**。数据量门槛 没到之前,所有指标都可以很难看。 2. 逃逸波与第三次 LR 跳回(r1050 重置回 1e-4)强相关:平台期被 周期性搅动后,突破发生在新周期的下降段(r1100-1149 场均 82.5%)。 LR 状态机的"谷底 + arena 驱动跳回"在逃逸叙事里是配角但关键。 3. 逃逸的确认信号按实际作用排序:**arena 持续区分 > 自对弈耗时 台阶 > 人机实测**。val policy loss 全程未掉头向下(§3.3 移动靶 效应),没有成为判据。 *** ## 5. 失败判据与回退策略 低 sims 不是信仰,是下注。设好止损线: | 信号 | 判定 | 动作 | | ------------------------------- | ------ | -------------------------------------------------------------------------- | | r1000+(约 10 万局)自对弈仍 <30 手、无耗时台阶 | 双盲期未逃逸 | sims 提到 100-200 重开(树深翻倍,战术可搜出)。注意 r300 远不足以判定——run\_1 r300 时仍双盲,r1200 自然逃逸 | | val value 也不降 | 管线故障 | 查标签/权重/学习率,与 sims 无关 | | 单轮耗时超预算 2 倍+ | 数据量失控 | 检查节点/局是否膨胀(随机模型摊平暂态),考虑 collect\_min\_frac>0 | | arena 长期对 champion 无区分 | 进步停滞 | 检查 LR 状态机是否在谷底滞留过久(run\_1 深平台 \~250 轮无区分,跳回后即突破,"长期"请以千轮尺度计) | 回退不是失败:参考实现的三支柱在其数据规模上成立,换算到不同 算力预算/网络规模时,sims 的最优值本来就该重新标定。 *** ## 6. 代码落位速查 如果你要在自己的实现里安放这些机制,典型落位如下: | 模块 | 内容 | | ------- | ----------------------------- | | 配置模块 | 全部探索参数(c\_puct/温度/开局/噪声/收集门槛) | | 自对弈工具模块 | 随机开局生成、层次重分配标签、全节点收集与权重 | | 搜索模块 | MCTS 主循环、根噪声、批量推理 | | 训练脚本 | LR 调度器(余弦+谷底+arena 跳回)、训练主循环 | *** ## 7. 一句话总结 **双盲期不是要"解决"的问题,而是要"穿越"的阶段**:用随机开局和 温度保证数据多样性不塌缩,用层次重分配保证低 sims 标签可用, 用终局信号喂活 value 头点燃逃逸正反馈,然后用对局长度和 arena 曲线监控穿越进度——剩下的交给总轮次(run\_1 实测: 1200 轮、约 12 万局、8 小时消费级显卡算力)。
Token 花在哪,能力就长在哪 -- 一次个人 vibe Coding 项目的工程复盘
# Token 花在哪,能力就长在哪 > 2026-09-20 · Mini Mall 项目复盘 > > 写给未来的我:当我又一次觉得"流程很完整、产出很有限"时,回来读这篇。 ## 一、先说遇到的问题 做 Mini Mall——一个走通"浏览商品 → 注册登录 → 购物车 → 下单 → 模拟支付 → 后台管理"的迷你电商——我耗掉了几个亿 Token。 先澄清一个容易误会的地方:这几个亿里,模型实验、废弃会话、反复试错才是大头,计划文档本身只占零头——前者是付钱买判断力,后者才是交的税,性质完全不同。所以这篇文章不是在算 Token 总量,而是在算去向:**每一笔消耗,最后沉淀成了什么。** 项目做完之后,我看到别人的同类项目:工程量大约是我的 1/5 甚至 1/10,做出来的页面和功能,看起来跟我的差不多。面对这个对比,有两种现成的解释。一种是"我浪费了"。另一种是"看起来差不多,不等于一样"——我的代码在边界处理、安全、权限、数据一致性、测试这些地方确实更扎实,这不是自我安慰。两种解释都不算错,但它们都没有回答我真正关心的问题: **在烧掉的那几个亿 Token 里,有多少沉淀成了代码质量和工程认知,又有多少只是堆积进了越来越详细的计划和流程?** 这篇复盘只回答这一个问题。 ## 二、我原本为什么这么做 这个项目起步是一个 Demo,规模不大:6 张表、24 个种子商品,没有并发、没有分布式。开始做之后,我主动做了一个决定:既然要认真做,就把自己查阅到的真实工程实践和 AI Coding 工作流带进这个个人项目,按照我理解的企业级工程方式来做——先设计后编码,核心逻辑写单元测试,边界认真对待,决策记录在案。 这个决定本身没有问题。事实是,后面的代码质量正是靠它撑起来的。 问题出在别的地方。 ## 三、计划是怎么一步一步做大的 先看项目 docs/ 目录的真实清单: ```text PLAN.md 233 行 总计划 PLAN_V0.md 355 行 阶段 0 执行记录 PLAN_V1.md 906 行 阶段 1 计划 PLAN_V1.1.md 1021 行 阶段 1 计划修订(只改执行流程) PLAN_V1.2.md 368 行 阶段 1 执行记录 PLAN_V2.md 799 行 阶段 2 计划 PLAN_V2.1.md 268 行 阶段 2 执行记录 ... ``` 一个"认证"阶段(11 个任务),我为它写了 906 行计划:每个任务做什么、跑什么命令、期望输出是什么,乃至实现代码,都逐段誊进了计划。之后为了调整执行方式,又写了 1021 行的修订版,序言里白纸黑字:"只改执行流程,不改交付范围。"执行完,再写 368 行执行记录。 计划里留着一个当时的实测数字:某个任务需要产出 31 行代码——计划里已经逐字写好的 31 行。实施者花了 50 秒、2 万 Token 把它们抄出来;审查者又花 54 秒、2.7 万 Token 把这次抄写审了一遍;中间还有一次等人的停顿。我当时得出的结论是:"慢的不是范围,是颗粒度。" 现在回头看,这句话只归因了颗粒度,却没有问一句:那 31 行已经写好的代码,当初为什么要誊进计划? ## 四、真正的问题:计划超过了项目的需要 难就难在,那套计划里的每一步,单独看都有道理。 计划细一点,执行时的歧义就少一点。多一次 Review,灌进来的缺陷就少一点。多一个 Pause 点,方向跑偏的概率就低一点。每一次增加,都感觉是在提高确定性——每一种感觉都是真的。 但这些"小幅增加"叠加起来,发生了两件事。 第一件:**计划从工具变成了目的。**一开始是"为了让项目有序完成而制定计划",后来变成"为了让计划完整而不断完善计划"。再往后,为了保证计划被正确执行,设计执行流程;为了保证执行流程被遵守,安排 Review、Pause 和验收;为了验证流程本身,再设计验证流程。一环扣一环,每一步都合理。结果是,一个 6 张表的项目,被 3,950 行过程文档驱动着往前走。规范服务项目,慢慢变成了项目服务流程。 第二件:**过程成本开始超过它保护的东西。**阶段 2 的验收是现成的例子:为了验证 11 条验收项,我写了一个探针脚本,脚本从 v1 改到 v5,出了 8 个 bug——正则吞掉点号、`grep -c` 的返回值被当成"不存在"、断言取反写反。8 个 bug 没有一个出在应用上,全是验收工具自己的问题。调试这个工具花的时间,超过了写应用本身。 到这里我终于看清了当时的处境:**计划本来是为了降低开发成本,后来却变成了开发成本的一部分。** 而且这和项目是不是 Demo 没有关系。就算这是一个真正的企业项目,我也一样可能把计划做过头。真正的问题不是工程标准太高,而是我渐渐失去了判断:这个标准当初为什么存在?在这个场景里,它现在还值这个成本吗? ## 五、不是所有多花的 Token 都是浪费 复盘的时候,我差点把这个结论推过头,把多花的 Token 全算成浪费。两件事把我拦住了。 第一件是 safeNextPath。登录成功后的跳转参数 `next` 不能允许站外地址,这是个典型的开放重定向问题。我前后做了三轮攻防:第一版的 `startsWith("/")` 判断被 `//evil.com`、`/\evil.com`、带 TAB 字符的载荷打穿;换成 origin 比对,又被 `http://internal.invalid//evil.com` 这种构造打穿;最后收敛成一个不变式——**输入和输出都必须是纯相对路径**。收尾时用 37 条载荷对抗扫描,0 条逃逸。 这三轮没有一轮白花:它改的是真实代码,给我的是真实攻击面的认知。协议相对地址、反斜杠归一化、占位 host 绕过,这些知识只在我亲手被打穿的地方长出来。到现在我还记得那些载荷的样子。 第二件是阶段末那轮十个角度的并行审查,从约 50 条原始发现里筛出三个真问题:并发注册同一邮箱会 500(实测 5 路并发、4 个 500);超长密码会被 bcrypt 静默截断到 72 字节;JWT 校验只信 `sub` 字段。每一个都是真实缺陷。 这些 Token 换来的东西写进了代码,也写进了我的工程认知。这是值得的。 真正该砍的是另一类 Token:**花在让计划更完整、让流程更漂亮、让过程记录更齐全上,最后没有产生对应的工程收益。** 把这次项目里的 Token 消耗摊开看,无非四类: | Token 去向 | 最终沉淀 | 是否值得 | | ------------------------------------- | ---------------- | -------- | | 探索未知、真实攻防、验证核心风险 | 工程认知 + 代码 | 值得 | | 核心业务逻辑:权限、金额、一致性 | 代码质量 | 值得 | | 重复规划、低风险重复 Review、流程包装 | 几乎没有新增能力 | 应该砍 | | 为了流程而维护流程 | 过程复杂度 | 应该警惕 | 判断标准只有一条:这笔 Token 最后沉淀成了什么。沉淀成代码、认知、风险发现,多半值得;只沉淀成更厚的计划和更完整的流程,那就要重新算账。 ## 六、我后来怎么改 意识到问题之后,我在后面的阶段做了几件具体的事。 阶段 3 到 5,docs/ 只新增了 3 份文档、共 378 行,全是执行记录,不再有独立的阶段计划。阶段的计划缩成 PLAN.md 里的一个小节;该拍板的决策在开工前一次性问完,执行中不再出现"决策 1/2/3、待裁定事项"。 审查从"每批次审 + 阶段末十路审 + 终审",改成每个阶段交付前审一次;已经足够明确的任务不再重复派 Agent;验收从调试探针脚本回到手动点一遍。有意思的是,探针脚本后来回来了——阶段 6 的 9 条验收、阶段 8 的 18 项渲染探针都靠它。但那时它是验收标准稳定之后的工具选择,不再是每阶段的惯性动作。 阶段 6 以后,不再新建任何过程文档:执行记录并入 PLAN.md 对应阶段,决策一行一条进 DECISIONS.md。 项目照常做完:阶段 0 到 8 全部收官,76 个 commit,136 个单元测试全绿,最后还落地了一套完整的视觉设计系统。砍掉的全是过程,没有一件是产品。 ## 七、最后的认识 我没有发现"认真做工程"是错的,也没有发现"企业级规范"是错的。 我发现的是:在认真做工程的过程中,我一度失去了工程判断。计划原本是工具;当我开始为了计划本身不断增加计划时,它就已经反过来消耗项目,而我没有察觉。 别人花 1/5 的 Token 做出看起来一样的东西,我的结论不是"应该照他们那样做",而是:我的过程里确实有一部分,他们省掉省对了。 这篇文章如果只能带走一件事,就是:**Token 消耗不是成本指标,Token 的沉淀方向才是**。以后再写代码,少问一句"花了多少 Token",多问一句"沉淀成了什么"。 真正的工程能力,不只是知道应该做什么,还包括知道什么时候已经够了。 > 练的是"企业级判断力",不是"企业级流程表演"。
盘活卫星数据资产!卫星平台 2.0 数据治理实战分享
每个数据平台都会经历三个阶段:"就这点数据?" → "怎么有点慢?" → "救命。" 我们决定不让第三阶段到来。 这是卫星平台 2.0 工程化笔记的第三篇。前两篇修了地基(数据库基线 + Flyway)和门锁(WebSocket 鉴权 + 三层 RBAC);这一篇处理的,是"房子住久了才会浮现的问题":一张只会长胖的遥测表、一堆无人认领的旧数据、一次可能噎死内存的导出、一本从来没人记过的账。 这周三件事:给遥测表**分抽屉**、给写操作**记账本**、给数据导出**装水龙头**——外加一次理直气壮的"构建失败"。每件事都有能搬回你自己系统的经验,建议先收藏。  --- ## 一、装水龙头:导出十万行,内存只端一只小碗 遥测页面新增了导出功能:选个时间范围,导出 CSV 或 JSON。 功能不难,难的是把它做"稳"。摆在后端面前最直觉的写法是:**查出全部数据 → 拼成响应 → 一次性返回**。行数少的时候岁月静好;行数一多,等于把一锅饭一次性塞进嘴里——几十万行对象同时躺在 JVM 堆内存里,OOM 不是"会不会",而是"哪一次"。 我们的做法是把它改成"水龙头":**细水长流,内存里永远只有一小碗。** 三道闸门,按顺序排队: 1. **先称重,再开箱**。导出前先用 `countInRange` 数一遍:超过 10 万行,直接拒绝并附一句人话提示"请缩小时间范围"。把风险挡在业务开始之前,而不是写到一半才崩。 2. **小碗分餐**。真正取数走 `forEachInRange`,每批 1000 行。内存峰值只由批大小决定,和"总共要导多少行"脱钩。 3. **边拉边写**。CSV 抓着 `OutputStream` 逐行写;JSON 用 `JsonGenerator` 流式吐数组。全程不构建"完整结果集"这个中间物。 几个值得抄的细节: - **CSV 开头写 UTF-8 BOM**。不加这个三字节,中文 Excel 打开就是经典"锟斤拷"名场面;逗号、引号、换行统一转义。 - **响应头带 `X-Total-Count`**,让前端知道"这锅饭有多少粒";文件名带时间戳,连点两次导出不会互相覆盖。 - **导出 14 列与页面表格严格一一对应**,抽样比对验证过——导出文件不是"另一份数据",就是页面那份。 还有一个前端的坑:项目里 axios 统一封装了业务响应拦截器(`R<T>` 那层),blob 下载不能走它——否则"下载文件"会被当作"业务响应"解析,喜提一个损坏文件。方案是独立 axios 实例 + 解析 `Content-Disposition` 文件名 + 失败时把 blob 解码回业务错误,把"为什么失败"还给用户。  --- ## 二、记账本:给每次写操作配一台"行车记录仪" 卫星平台这类系统,"谁改过轨道参数"是必须答得上来的问题。改造之前,答案是:答不上来。 方案本身很轻:一个 `@AuditLog` 注解 + 一个 AOP 切面。不熟 AOP 也没关系,一句话解释——**不用改任何业务代码,在方法的前后自动"夹带"一段记录逻辑**。给写操作方法贴个注解,剩下的交给切面。 每笔"账"记录八个要素:谁、何时、对什么资源(含资源 ID)、做了什么、结果如何、失败原因、来自哪个 IP 和 User-Agent、耗时多少。落到 `audit_log` 表(Flyway V2 迁移,4 个索引,`IF NOT EXISTS` 保证幂等)。 三个设计决策,每个都配一条"为什么": 1. **审计是旁路,不是主链路**。落库整段包 try-catch——审计写失败,业务照样成功。记账的不能耽误开车的。 2. **匿名请求直接跳过**。登录接口那一刻还没有身份,不产生"查无此人"的废记录。 3. **IP 三级回退**:`X-Forwarded-For` → `X-Real-IP` → `remoteAddr`。请求过了 Nginx 之后,`remoteAddr` 拿到的是代理的地址,得像查快递一样逐级回溯真实来源。 还有一条"克制"原则值得单独说:我们只接了 8 个 Controller、约 30 处**写操作与导出**;仿真里调速、暂停这类纯展示交互一律不接。**审计不是越多越好**——每天几万条流水里找一条异常,等于没有审计。 查询侧配套交付:`GET /audit-logs`(ADMIN 限定)+ 前端审计页(按操作人 / 动作 / 资源 / 结果 / 时间范围筛选)。身份传递用了最轻的方式:userId 挂到 `authentication.details` 上,不动 principal 类型,存量鉴权代码零感知。  --- ## 三、自曝:我们主动让构建变红了 这一节讲一次"反直觉"的交付。 覆盖率门禁用的是 JaCoCo:行覆盖 ≥ 70%,`service`、`dataaccess` 包 ≥ 80%,绑定在 `mvn verify` 上。门禁绑定的那天,基线长这样: | 指标 | 基线值 | 门槛 | | ----------------------- | ----------------- | ---- | | 总行覆盖率 | 10.7%(294/2749) | 70% | | service.impl | 12.5% | 80% | | dataaccess / controller | 0% | 80% | 摆在我们面前有两条路:A,把阈值降到"现在就能过";B,让构建红着,把缺口精确列出来。 我们选了 B。 理由很简单:**门禁的意义不是展示绿灯,而是让欠账一直看得见**。绑定后 `mvn verify` 会精确报出 4 项违规——这就是一份写给未来的还债清单,而不是一个"大家都假装没看见"的数字。补测冲刺已经排进 M2 第一天;现在让构建红着,是为了让它更快地真正变绿。 同一批还顺手做了缓存精细化(原计划里的弹性项): - **TTL 分层**:默认 10 分钟、仪表盘 1 分钟、实体详情 30 分钟——冷热数据分开对待; - **`@CacheEvict` 精准失效**:改、删按 ID 踢对应的 key;新增不需要踢;只有批量删除才整片清。 期间挖出一个静默的坑,值得单独讲讲:**自定义 `RedisCacheConfiguration` Bean 会全盘接管 yml 里 `spring.cache.redis.*` 的所有配置**。你在 yml 里改 TTL、配 key-prefix,全部静默失效、还不报错。修复之后我们补了测试把 TTL 契约钉死——缓存配置这种东西,"悄悄退化"比"当场报错"可怕得多。  --- ## 四、分抽屉:遥测表按天归档,过期整屉清走 终于说到这周的主角。 遥测数据是典型的"只进不出":每一秒都在写入,且永远不嫌多。用传统思路清理旧数据,两条路都不好走: - **不删**:查询的扫描量跟着时间一起涨,一年后查一天的数据,数据库要翻遍整个"仓库"; - **`DELETE` 删**:注意,`DELETE` 只是"标记删除"——空间不还、索引持续膨胀、VACUUM 追在后面跑。 分区表给的是第三条路:**按天分抽屉**。查询只开对应日期的抽屉;过期数据不一本本撕,直接把整个抽屉端走(`DROP PARTITION` 是元数据操作,秒级回收)。 ### 这次迁移里的四个硬核细节 **1)一个反直觉的硬规则:主键必须包含分区键。** PostgreSQL 声明式分区下,唯一约束必须涵盖分区列,否则建表直接被拒。我们的主键从 `(id)` 改成了 `(id, "timestamp")`——第一次写分区迁移脚本的人,八成会在这里撞墙。 ```sql -- V3__telemetry_partition.sql(节选示意) CREATE TABLE telemetry_data (..., PRIMARY KEY (id, "timestamp") -- 分区键必须进主键 ) PARTITION BY RANGE ("timestamp"); ``` **2)迁移"全有或全无"。** 整个 V3 迁移跑在单事务里 + 幂等守卫:旧表让名 → 建分区父表 → 按「历史数据日期 ∪ 昨天~未来 7 天」建按日分区 → 搬数据 → **行数强校验** → 校验不过 `RAISE EXCEPTION` 整体回滚 → 删旧表、重建 4 个索引。**宁可重来,不留半成品**——数据迁移最怕的不是失败,是"成功了一半"。 **3)搬存量用"编号"找下一批,不用 `OFFSET`。** 5000 行一批、以 id 为游标往前推。为什么不用看起来更顺手的 `OFFSET`?因为在大表上,深翻页的 `OFFSET` 每次都要重新"数过前面所有的行",越翻越慢;而且大批量操作拖得越久,锁表风险越高。搬家公司的正确姿势,是记住"最后一箱的编号",而不是每次都从第一箱数起。 **4)光分区没用,查询得"带着抽屉号来"。** 如果 SQL 不带分区键条件,优化器照样给你全表扫——"分了但没用"。所以 `page` / `countInRange` / `forEachInRange` 的时间窗做了缺省补全(不传就用 `[近 91 天, 明天]`),显式传的边界原样保留。保证每一条查询都携带分区键,裁剪才真正发生。 ### 自动打理与开机自愈 分区维护不需要人管: - **每天凌晨 02:30**:预建"昨天 ~ 未来 7 天"的分区(防止跨天凌晨"抽屉还没到货"),顺手清掉 90 天前的分区; - **应用启动时自愈一次**:出差三天没开服务?重启那一刻,它自己把欠的分区补上、过期的清掉,失败只告警、不阻塞启动; - **双重防呆护栏**:自动 DROP 前,分区名必须匹配 `telemetry_data_pyyyyMMdd` 的命名,且边界能被 `pg_get_expr` 解析出来——**只扔自家抽屉,隔壁系统的表碰都不碰**。自动化想获得"自主行动"的资格,先证明自己知道边界在哪。 ```mermaid flowchart LR A["遥测写入<br/>自动进当天抽屉"] --> B["查询带时间窗<br/>只开相关抽屉"] B --> C["每天 02:30<br/>预建未来 7 天"] C --> D["满 90 天<br/>整屉 DROP"] ``` ### 实测数据 验证走的是"两阶段"路线,很值得借鉴:先用 Flyway 的 `target` 参数把库**停在中途版本(V2)**,预置 5 行数据(3 条近期 + 2 条远期),再全量升级: - 迁移日志:**"搬移完成:5 行(批大小 5000)"**,行数校验通过; - 启动自愈:超期分区被自动识别并清理; - 写入冒烟:新数据正常落进当天分区,序列继续递增; - `EXPLAIN` 实测:**单日窗口只碰 1 个分区;7 天窗口只碰 4 个相关分区**——没有全表扫,也没有"假装分区"式的全分区扫。 配套新增 14 条单测(清理服务 9 条 + 查询窗口 5 条),后端全量 **46/46** 全绿。   --- ## 五、一周数字快照 | 验收项 | 方法 | 结果 | | ------------------- | -------------------------- | -------------------------------------------- | | 后端单元测试 | `mvn test` | **46/46 全绿**(分区相关新增 14 条) | | 分区裁剪 | `EXPLAIN` 实测 | 单日窗口仅扫 1 个分区;7 天窗口仅扫 4 个 | | 存量搬移 | 临时库两阶段验证 | 行数强校验通过,日志留痕 | | 过期清理 | 启动自愈实跑 | 超期分区自动 DROP | | 导出上限守护 | 单测 | 超 10 万行拒绝并给出提示 | | 审计(切面 + 查询) | 单测 8 条 | 成功 / 失败 / 落库失败 / 匿名 / IP 回退全过 | | 前端门禁 | `npm run lint` / `vue-tsc` | 0 error | | E2E 回归 | `npm run test:e2e` | 7 passed + 3 skipped | | 覆盖率门禁 | `mvn verify` | **"诚实的红"**:4 项违规精确列出(见第三章) | --- ## 六、这周最值的 7 个坑(直接抄作业) 1. **PostgreSQL 分区表的主键必须涵盖分区键列**。想写 `(id)` 的请收手,建表会直接失败。 2. **大表搬移:游标分批 + 行数强校验**,校验不过就整体回滚。宁可重来,不留半成品。 3. **清理旧数据用 DROP 分区,不用 DELETE**。一个把抽屉整个端走,一个只是在报纸上划了道线。 4. **流式导出三件套**:先 count 称重 → 分批拉取 → 边写边刷;CSV 记得 UTF-8 BOM,不然中文 Excel 满屏"锟斤拷"。 5. **审计切面落库必须 try-catch**。审计是旁路,永远不能拖垮主业务。 6. **自定义 `RedisCacheConfiguration` Bean 会静默接管 yml 缓存配置**。TTL 改了没反应时,先查有没有这个 Bean;再用测试把契约锁死。 7. **自动清理必须带防呆护栏**:命名格式 + 边界解析双重校验,才配得上"自动"两个字。 --- ## 七、接下来:M2 主战场 - **告警规则引擎 + 通知中心**:阈值 / 区间 / 持续时间三类规则,越限 → 站内信 + WebSocket 推送 → 顶栏铃铛亮起,不再靠人盯屏; - **认证加固三件套**:图形验证码、失败锁定(5 次锁 15 分钟)、密码强度策略; - **可观测性**:Prometheus + Grafana 看板 + Loki 日志检索,关键指标配置告警; - 还有一件要还的账:**补测冲刺**,让 `mvn verify` 从"诚实的红"变回"健康的绿"。 --- ## 写在最后 这三件事没有一件是"卫星专属"——任何系统只要活得够久,都会遇到同样的三堵墙:**表在长胖、账没人记、导出随时崩**。对应的解药也就三句话: **数据有出口(流式导出)、操作有账本(审计留痕)、旧数据有去处(分区清理)。** 如果这篇帮你绕开了哪怕一个坑,欢迎**点赞、在看、转发**给可能用得上的同事。留言区聊聊:你们的过期数据,是 DELETE 掉的,还是 DROP 分区掉的? --- *本文关键词:遥测分区 | 流式导出 | 审计日志 | AOP | PostgreSQL | JaCoCo 覆盖率门禁 | Redis 缓存 | 卫星平台 2.0*
我自己做的音频收取工具43 项测试全绿,程序里却躺着 3 个真实缺陷
我以为一下午能写完的转码小工具,最后栽在三个「测试全绿」的缺陷上 我想做个东西,把一堆 mp4 的音频批量抽出来存成 MP3,音质能自己选。 按理说这是个下午茶的活。Python 生态里现成轮子一抓一大把,套个 tkinter 界面就完事,代码量撑死几百行。 然后这件事从头到尾都在打我的脸。不是因为它难,而是因为我几乎每一个「想当然」,最后都被证明是错的。 先摸底,第一脚就踩空 动手之前照例先看环境。这一看就发现问题了,这台机器上根本没有 ffmpeg。 更迷惑的是,pip list 里赫然躺着一个叫 ffmpeg 的包,版本 1.4。名字对得上,版本号也挺齐全,看着就像那么回事。 真相是,这个包是个空壳,它只负责转发调用系统的 ffmpeg,自己一点二进制都不带。 而「从 mp4 里抽音频」这件事,本质上必须有 demuxer 和 decoder。我顺手查了下那几个常用库,moviepy、pydub、librosa,底层全都依赖 ffmpeg,一个都绕不开。 所以「怎么搞到 ffmpeg」不是可选项,它才是这个项目的第一个架构决策。 摆在面前三条路。让用户自己装,程序启动时探测 PATH。或者首次运行时联网下载。或者找那种 pip 装完就能直接用的。 我选了第三条,imageio-ffmpeg。 理由很简单,前两条都会把麻烦转嫁给使用者。这个工具最后是要打包发给同事的,如果同事拿到 exe 之后还得自己去官网下 ffmpeg 配环境变量,那打包这件事本身就失去意义了。 定下来之后我没有直接开写,而是先花时间验证了一件事,这个包里到底有没有 MP3 编码器。 因为 ffmpeg 本身并不自带 MP3 编码能力,它靠的是 libmp3lame 这个外部库。如果随包附带的构建恰好是精简版没带这个库,那整个项目的前提就不成立,前面所有选型都白做。 跑完确认,libmp3lame 在。地基稳了。 差点被一个字段名骗过去 真正开始写代码之后,我挖出一个很阴险的坑。 ffmpeg 有个 -progress 参数,能把进度以机器可读的 key=value 形式吐出来。我本来打算读其中的 out_time_ms 来算百分比,名字看着特别直白,毫秒嘛。 实测输出长这样 out_time_us=3018594 out_time_ms=3018594 一个 3 秒的文件,这两个字段的数值一模一样。 也就是说 out_time_ms 这个名字本身就是错的,它的值根本不是毫秒,而是和 out_time_us 相同的微秒。这是 ffmpeg 一个长期存在的历史遗留问题,字段名写错了但一直没改,因为改了会破坏兼容性。 如果我照着字段名当毫秒用,算出来的进度会放大一千倍,3 秒会被当成 50 分钟。表现出来就是进度条纹丝不动,然后在最后一瞬间突然跳满。 这种 bug 最烦人的地方在于,它不报错、不崩溃,只是安静地给你一个错误的结果。而且后面任何一个人读这段代码,都会觉得「读 out_time_ms 有什么问题」。 我在代码里留了注释说明原因,还专门写了个测试把这个坑钉死,断言的写法就是「如果按毫秒算,结果会差 1000 倍」。 我以为它 25 MB,实际是 87.6 MB 选型的时候我脑子里有个大概印象,ffmpeg 的静态构建差不多二十几兆。 打包出来一看,傻了。那个二进制文件是 87.6 MB,单独一个文件就占了整个包的 85%。 我把包解开看了体积构成,除了 ffmpeg 之外,Python 运行时加上各种依赖库总共才十几个兆。整个 111 MB 的解压体积里,ffmpeg 是绝对主角。 这个数字直接改变了后面的一个决策。 单文件 exe 的卖点,其实是假的 同事要的是「双击就能用」的东西。我第一反应是打包成单个 exe,干净利落,发一个文件过去多省事。 但我不太喜欢凭印象下结论,就实际构建了两种形态来测。 结果挺打脸的。 单个 exe,38.5 MB,每次启动 9.30 秒。 文件夹形态压缩成的 zip,39.7 MB,每次启动 0.66 秒。 体积几乎一样,启动速度差 14 倍。 原因在于「单文件」模式每次运行都要把整个包解压到临时目录。而它唯一的卖点,那个「只发一个文件」的便利,在体积上根本没兑现,因为 ffmpeg 那 87 MB 压缩之后也就三十来兆,两种形态的压缩率没有区别。 所以单文件的优势是想象出来的,代价却是每次都要等 9 秒。双击之后对着空屏干等 9 秒,用户第一反应肯定是程序坏了。 最后选了文件夹方案,压缩成一个 zip 发出去。 测试全绿,但程序是坏的 这部分是整件事里最值得说的。 代码写完之后我跑了 43 项测试,全绿。如果就此收工,交付出去的会是一个有缺陷的东西。 但我决定真的把程序跑起来看一眼。这一看,问题全出来了。 第一个缺陷,一个纯视频(没有音轨)被标记成了「失败」。可我设计的时候明确写过,这种情况应该标记为「跳过」,而且不计入失败总数。 往深了想还有一层。探测数据其实能区分两种情况,如果一个文件能读出正常时长但找不到音轨,那是真的没有音轨;如果连时长都读不出来,那是文件损坏。这两种情况在界面上应该给出完全不同的提示,我却把它们混成了一类,都报「未检测到音轨」。用户看到一个正常的无声视频和看到一个损坏文件,得到的信息是一模一样的。 第二个缺陷更吓人。 我在入口脚本里加了个自检功能,跑起来却始终返回退出码 1,但报告内容明明白白写着一切正常。 查下来是两个 bug 叠在一起。一是入口文件里漏了 import os,而自检代码里用到了 os.path.isfile。二是我的崩溃兜底写成了 except BaseException,而 sys.exit(0) 抛出的 SystemExit 正好是它的子类,于是所有正常退出都被自己的兜底捕获,反手改成了退出码 1。 但这里有个细节值得单独拎出来说。 那个 import os 的问题,在打包后的版本里根本没有暴露。 因为 PyInstaller 的冻结运行时恰好把 os 注入到了全局命名空间,程序侥幸能跑。 也就是说,如果我只测打包版本,这个 bug 永远不会被发现。而它一旦在别的环境里触发,就是启动即崩,用户看到的是「双击没反应」。 靠环境巧合掩盖的 bug 是最危险的,因为你所有的验证都会告诉你「没事」。 还有一个假阳性,是我自己造的 我还写了个冒烟测试,驱动界面跑完整流程。测到「中途取消」这一步时,它显示通过。 但输出里有一行让我起了疑心,它说取消时正在运行的任务数是 0。 也就是说,那次取消打断的只是排队等待的任务,压根没碰到正在运行的进程。而我真正想验证的那条路径,进程强杀之后半成品文件能不能清理干净,其实根本没被覆盖到。 原因是我在测试脚本里用了 time.sleep 等待。这期间 tkinter 的事件循环是停摆的,界面状态当然不会更新,我看到的「没有正在运行的任务」纯粹是个假象。 改成边等待边泵事件循环之后,测试才真正打断了两个正在运行的进程。结果符合预期,输出目录里一个残缺文件都没留下。 但如果我没有多看那一眼输出,我会一直以为这条路径已经验证过了。 写在最后 这个工具本身没什么了不起的,就是把 mp4 的音频抽出来存成 MP3,支持五档音质,并行转换,窗口里能看到每个文件的进度和状态。 但它让我重新确认了几件事。 别信自己的印象,去测。 我以为 ffmpeg 是 25 MB,实际 87.6 MB。我以为单文件 exe 只是稍微慢一点,实际慢 14 倍。这两个数字,都直接改变了我的技术决策。如果我按印象走,交付出去的就是一个双击要等 9 秒、同事以为坏了的程序。 测试通过和程序能用,是两件完全不同的事。 43 项测试全绿的时候,程序里躺着三个真实缺陷。它们都不在测试覆盖的范围内,只有在真的把程序跑起来、真的去逐字看输出的时候才会现形。 靠巧合成立的代码最危险。 那个漏掉的 import os 在所有打包测试里都毫无症状,因为构建工具恰好替它兜了底。而侥幸这件事,不会一直侥幸下去。 那个自检功能我最后保留了下来。因为打包后的程序是没有控制台的,出了问题用户只会看到「双击没反应」,完全无从排查。现在让同事跑一句带 --selftest 的命令,就能拿到一份报告,明确告诉他到底是哪里不对。 顺便说,这个项目还有个绕不过去的现实问题。包内那份 ffmpeg 是 GPL v3 授权的,随程序一起分发给同事,严格来讲构成 GPL 意义上的再分发。内部使用没人会追究,但这属于应该知情的事。如果哪天要对外发布,换成 LGPL 构建就行,代价是少部分编码器不可用,不过这个项目只需要 libmp3lame,LGPL 版本同样有。 工具已经打包好了,43 MB,解压双击就能用。 你在自己的项目里,有没有遇到过那种「测试全绿但其实是坏的」的情况? 
做 Coding Agent 搜代码:到底该选 RAG 还是 Grep?扒完 Claude Code 源码我顿悟了
大家好,我是不会喷火的小火龙。 很多团队自研 Coding Agent,第一步就是去搭向量数据库——Chroma、Qdrant、Weaviate 随便选一个,跑 Embedding,切片,建索引。看起来很"AI",很高大上。 但你有没有想过:Claude Code,目前公认最强的 AI 编程工具之一,它的代码检索底层,其实就是一个 50 年前的命令行工具——`grep`? 如图 1 所示,这就是很多团队盲目上 RAG 之后经历的真实工程困局,和 Anthropic 自己走过的弯路。  这篇文章想帮你搞清楚三件事:代码检索场景下 RAG 为什么频繁翻车、Claude Code 的极简方案如何跑通、以及你自己的项目到底该怎么选。 --- ## 一、为什么代码检索场景下,传统 RAG 频频翻车? 代码是符号系统,不是自然语言。这句话听起来像废话,但绝大多数 RAG 翻车的根源就在这里。 向量语义检索的逻辑是意思相近的文本在向量空间里彼此靠近,这在自然语言里成立,但在代码里会系统性失效。举个例子:`createD1HttpClient` 和 `buildD1HttpClient` 语义高度相近,向量距离极小,但它们在代码里是两个完全不同的函数;而同一条调用链里相互依赖的函数,语义可能毫无关联,向量检索根本找不到它们之间的联系。 如下表所示,代码符号场景下,语义向量召回和精确匹配之间的准确率差距相当触目: **代码精确符号 vs 语义向量召回偏差对比** | 查询类型 | 示例查询 | 向量检索结果 | 精确匹配结果 | |:---|:---|:---|:---| | 函数名精确查找 | `createD1HttpClient` | 召回 `buildD1HttpClient`(相似但错误)| 精准命中,第 1 条结果 | | 错误码定位 | `ERR_DB_CONNECTION_TIMEOUT` | 召回语义模糊的错误描述文本 | 直接定位到常量定义行 | | 调用链追踪 | `handlePaymentCallback` 的全部调用方 | 召回支付相关的无关业务代码 | 逐个匹配调用点,一个不漏 | | 变量赋值查找 | `MAX_RETRY_COUNT = 3` 在哪里定义 | 无法区分定义和引用 | 精确定位定义行 | RAG 在代码场景还有三个硬伤。 第一,Chunk 切片会破坏 AST。按字符数盲目切片,会把一个类或一个函数切成两半,上下文彻底断裂。第二,索引永远赶不上代码变化。代码是秒级变动的,向量索引构建慢、增量同步复杂,极易产生脏数据,让 LLM 基于过期上下文产生幻觉。第三,全量代码送第三方向量化,隐私泄露风险不可忽视;本地跑大型 Embedding 模型,GPU 成本直接劝退。 --- ## 二、扒开 Claude Code 源码:极简的 Agentic Search 是怎么跑通的? Anthropic 没有预先构建任何静态索引。Claude Code 的做法是把搜索变成 LLM 的"多轮自主探索循环"。 三大并发安全工具构成整个检索引擎: - `GlobTool`:按文件名模式快速扫描项目骨架,类似人类第一步看目录结构,确定大致位置。 - `GrepTool`(底层是 ripgrep):毫秒级全文精确正则搜索,类似人类在 IDE 里按 `Cmd+Shift+F` 搜关键词。 - `ReadTool`:按需读取具体行号范围,类似人类定位到文件后翻开那几十行上下文。 为什么选 ripgrep 而不是普通 grep?ripgrep 用 Rust 开发,内置 SIMD 硬件加速,万级文件的全文扫描仅需约 200ms,自动跳过 `.gitignore` 指定的目录,天然适配代码仓库。内置的 `head_limit=250` 截断保护,防止搜索结果把上下文窗口撑爆。 如图 2 所示,两者的底层架构存在根本性的范式转移: **图 2:静态 RAG 检索管线 vs Agentic Search 多轮自主探索流程**  这个设计里最巧妙的地方:LLM 本身就是最顶级的动态 Reranker。每轮检索结束后,LLM 自己判断这个线索够不够、要不要继续顺藤摸瓜,完全替代了传统架构里那个静态的 Rerank 模型。 --- ## 三、权威佐证:Anthropic 踩坑实录与亚马逊论文实锤 放弃 RAG 不是 Anthropic 拍脑袋的激进决定,是被真实实验数据逼出来的。 Boris Cherny(Claude Code 技术负责人)在 2025 年 5 月的 Latent Space 播客里说过:Claude Code 早期确实搭建过基于 Voyage Embedding 的向量 RAG 检索系统,效果"还行"。但在实测用 Glob + Grep + Read 构成的 Agentic Search 之后,他的原话是: > "outperformed everything, by a lot. You avoid all the problems with security, staleness, and indexing." 安全问题没了,索引过期没了,增量同步也不用维护了。 学界的数据同样说明问题。亚马逊科学团队在 AAAI 2026 发表的论文《Keyword search is all you need》([arXiv:2602.23368](https://arxiv.org/abs/2602.23368))严格对比了标准向量 RAG 和纯关键词检索的 Agent 系统,结论是:关键词搜索在综合性能上达到 RAG 的 90% 以上,在代码等高度结构化的符号系统中,精确匹配的表现还显著优于向量语义检索。 从工程实践到学术实验,两份证据指向同一个结论:代码这个特殊领域里,精确性比语义相似性更重要。 如图 3 所示,代码搜索工具链使用图:  --- ## 四、反方视角:Cursor 为什么坚持重仓向量? 读到这里,你可能会问:Cursor 呢?它也是 AI 编程工具里的顶流,技术路线和 Claude Code 截然不同,难道走错了? 没有。两者的架构选择,背后是产品形态的差异。 Claude Code 是轻量 CLI 工具,面对频繁切换的项目,零冷启动是第一要务。每次打开一个新项目,没有时间也没有必要预建索引,Glob + Grep 随开随用,几毫秒就能工作。 Cursor 是 IDE 插件,常驻用户系统,面对的往往是几十万文件的超大 Monorepo。在这个规模下,每次查询都做全量 ripgrep 扫盘,性能会出现瓶颈。 如图 4 所示,Cursor 并没有抛弃 grep,而是构建了一套双轨混合系统: **图 4:Cursor 混合检索双轨架构**  Cursor 工程团队在官方博客([Instant Grep & Codebase Indexing](https://www.cursor.com/blog/instant-grep))里直说:纯语义搜索在精确标识符、错误码、函数名上表现太差,所以他们自研了基于三元组(trigram)倒排索引的"Instant Grep"来弥补。两套系统并联,精确归精确,语义归语义,用 Merkle Tree 实现分钟级增量同步保持索引新鲜。 Cursor 的一个典型场景是:用户说"微信支付退款重试逻辑在哪里",记不住具体函数名,这时候语义向量检索才真的有用。 --- ## 五、实战选型指南:自研 Coding Agent 到底怎么选? 扔掉非黑即白的架构争论。正确的问题是:你的产品形态和业务场景是什么? 我把这套选型逻辑整理成了一张清晰的决策树,如图 5 所示: **图 5:Coding Agent 代码检索技术选型决策树**  三个场景的核心判断: 模式 1,极简优先,适合 90% 的个人开发者和轻量 CLI 工具。零依赖、零维护,直接用 ripgrep + Glob + Read 先把产品跑起来,有实际用户反馈再说向量的事。 模式 2,混合检索,适合 IDE 插件和常驻大型 Monorepo 的服务。精确符号归 trigram 倒排,模糊语义归轻量向量,两者并联。不要上重型托管向量数据库,自研 trigram 索引成本比你想象的低得多。 模式 3,LSP 语法感知,有明确跨文件引用追踪需求时接入 Language Server Protocol,获取符号定义和引用关系图。这才是"语义理解"真正应该发生的地方。 --- ## 结语 Anthropic 用 50 年前的 grep 打败了自己搭的向量 RAG,这不是反智,这是工程上的诚实:代码是精确的符号系统,grep 是精确的符号匹配工具,用对了工具比堆技术栈更管用。 希望这篇文章能帮你在自研 Coding Agent 的路上少走一段弯路。 如果你对这类技术拆解感兴趣,欢迎关注公众号「[小火龙AI 手记](https://mp.weixin.qq.com/s/1NIIz2-4XnP8cSDmr9Fa-g) 」。 这里持续挖掘 GitHub 等开源社区里真正好用、能打的高价值项目,拆解它们背后的工程逻辑;同时也记录我自己用 AI 搓工具、做产品、踩坑填坑的全过程实录。都是有一点技术基础就能看懂的干货,跟你一起把 AI 驯化成真正趁手的生产力工具。 --- *参考资料:* - *Boris Cherny & Cat Wu on Claude Code,Latent Space Podcast,May 2025:https://www.latent.space/p/claude-code* - *Claude Code 官方文档:https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview* - *Shreyas Subramanian et al.,"Keyword search is all you need",AAAI 2026:https://arxiv.org/abs/2602.23368* - *Cursor Engineering,"Instant Grep & Codebase Indexing":https://www.cursor.com/blog/instant-grep*
chatgpt
从玩具 Demo 到稳定交付:个人开发者的 3 条 AI 编程工程铁律
大家好,我是不会喷火的小火龙。 最近半年,我的私信和评论区被同一类问题淹没了: > "小火龙,我用 Cursor 10 分钟做了个 App,本地跑着没问题,一部署就各种报错,改了一下午越改越烂……" > "AI 帮我写了个后台管理系统的 Demo,看着挺好的,我加了两个需求之后整个项目就崩了,连回退都回退不动。" 这些问题指向同一个根源。今天我把这件事说透。 --- ## 一、喝着咖啡看 AI 刷屏很爽,直到我第一次尝试把它部署上线 下面这张图基本就是大部分人的真实状态。  Vibe Coding 这个词是 Andrej Karpathy 在 2025 年 2 月造的。他当时在推特上说: > "There's a new kind of coding I call 'vibe coding', where you fully give in to the vibes, embrace exponentials, and **forget that the code even exists**." 翻译一下:给 AI 一句话描述你要什么,然后 Accept All,不读代码、不看 diff、不关心底层实现。跑不通就把报错贴回去,通常它就自己修好了。 但 Karpathy 自己紧跟着补了一句,很多人选择性忽略了: > "**It's not too bad for throwaway weekend projects**, but still quite amusing." 注意 **throwaway weekend projects** 这几个词。用完即弃的周末玩票项目。 你拿着这套方法,想做一个要长期维护、要部署上线、要给真实用户用的产品,那就好比拿一次性筷子去炒铁锅大灶,不折才怪。 我自己也经历过一模一样的过程。去年用 Claude Code 做一个数据看板工具,前 40 分钟觉得"AI 时代来了,编程已死"。然后我加了一个用户权限模块,AI 在写权限的同时悄悄改了数据库的字段命名规则,所有查询接口全挂了。我花了整晚把项目恢复到能跑的状态。 后来我发现,几乎所有用纯 Vibe Coding 做"正经事"的人,都在经历同一个循环: 10 分钟出 Demo → 觉得自己是天才 → 加需求 → AI 连环翻车 → 越修越烂 → 推倒重来。 --- ## 二、为什么 Vibe Coding 一碰复杂工程就必成屎山? 很多人把翻车归结于"AI 还不够聪明"。不对,问题出在 Vibe Coding 这套工作方式本身。 ### 沉默假设:AI 替你做了一百个决定,但没问过你一次 当你给 AI 一句"帮我加个用户登录功能",它需要自行决定至少十几个技术问题:用 session 还是 JWT?密码存 bcrypt 还是 argon2?数据库加哪些字段?路由怎么组织?错误码怎么定义? AI 不会说"我不确定,先问问你"。它悄悄选一个最常见的方案直接写下去。这些沉默假设累积起来,你的项目被塞了大量你不知道的技术决策。下次你再加需求,新代码和旧的隐式决策冲突,系统就崩了。 ### 改一个点,炸三个面 大模型按 token 序列预测下一个字符,注意力集中在"当前这段对话要解决什么"。它没有一个独立运行的"架构守护进程"来检查跨模块的依赖。 所以你说"把这个按钮改成蓝色",它可能在改 CSS 的同时顺手"优化"了组件的 props 接口。你其他三个页面引用了这个组件,全挂了。 这不是 AI 犯蠢,是工作模式使然。如图 2 所示,纯聊天模式和有规范约束的模式,在复杂度增长后走向完全不同:  左边是个发散循环,复杂度指数膨胀直到崩盘。右边通过先收敛方案、再局部执行、最后测试验证,把失控风险限制在每次迭代的小范围内。 ### 你省掉的那些"无聊步骤",恰恰是软件工程的地基 Simon Willison(`sqlite-utils`、`datasette` 的作者)在谈用 LLM 写代码时说过一条规矩: > "My golden rule for production-quality AI-assisted programming is that **I won't commit any code to my repository if I couldn't explain exactly what it does to somebody else**." 大白话就是:如果你没法给别人讲清楚这段代码在干嘛,就不该把它合进项目里。 Vibe Coding 恰恰跳过了这一步。你 Accept All 的时候不知道 AI 改了什么,也没跑测试验证它改得对不对。你在没有安全网的钢丝上裸奔,功能稍微复杂一点,摔下来就是必然的。 --- ## 三、治好 AI 乱写屎山的 3 条防翻车铁律 我在过去半年的项目里总结出 3 条规则,配合 Cursor、Claude Code、Windsurf、Copilot 都能用。遵守这 3 条不会让 AI 编程变慢,反而会省掉返工的时间。 ### 铁律 1:范围与权限隔离 每次只准 AI 动你指定的文件,不准它自作主张碰别的地方。 这条最容易执行,效果也最直接。AI 乱改代码的根本原因是你给了它全库漫游的权限。你说"帮我修个 Bug",它可能把整个项目的目录结构重新组织了一遍。 具体做法: 1. 在 prompt 里明确列出允许修改的文件路径。比如"只修改 `src/components/LoginForm.tsx` 和 `src/api/auth.ts`,不要碰其他文件"。 2. 严禁 AI 擅自新建文件和引入新依赖。需要新文件或新库,要求它先说明理由,你确认后再建。 3. 单次任务只解决一个问题。不要一句 prompt 里塞三个需求。 GitHub 上最近有个项目叫 `ponytail`,它的 README 写了一句话我觉得总结得到位: > "Makes your AI agent think like the laziest senior dev in the room. The best code is the code you never wrote." 让你的 AI 像团队里最懒的高级工程师一样思考。最好的代码是你根本不用写的代码。  ### 铁律 2:规格与意图前置 让 AI 先说清楚它打算怎么改,你同意了它再动手。 在 AI 动手写代码之前,要求它先输出一份简短的变更方案(Spec),包含: - 准备修改哪些文件 - 每个文件改动的具体逻辑 - 涉及哪些接口或数据结构变更 - 有没有潜在的副作用 你审核这份方案,觉得没问题,再让它写。 这一步可能就 30 秒,但能省掉后面 30 分钟的连环修 Bug。绝大多数翻车,都是 AI 在方案阶段就跑偏了,你在这一步拦住它,后面的代码天然不会乱。 具体的 prompt 模板: ``` 在写代码之前,先用中文给我一个简短的变更方案,包含: 1. 需要修改的文件列表 2. 每个文件的修改逻辑(2-3 句话) 3. 涉及的接口/数据结构变更 4. 潜在的副作用或风险 等我确认后再开始编写代码。 ``` 这段话可以直接写进项目的 Rules 文件里,AI 每次开始工作都会先过这个流程。 ### 铁律 3:自动化验证闭环 AI 写完代码必须自己跑通测试,跑不通就继续改,不要让你当人肉审阅器。 很多人的做法是 AI 写完了,自己肉眼看一遍 diff,感觉差不多就合并了。这样做等于把质量保障全压在你的肉眼上,而你的肉眼不可能看出所有边界条件和隐式依赖。 在项目规则里要求 AI 每次修改后自行运行测试命令。跑不通就继续修,直到测试全绿。你要做的是验收,不是 Debug。 在 Rules 里加上这样一段: ``` 完成代码修改后,必须运行以下验证命令并确认全部通过: - `npm run typecheck`(或对应的类型检查命令) - `npm run test`(或对应的单元测试命令) - `npm run build`(确认构建不报错) 如果任何一项未通过,自行修复后重新运行,直到全部通过再提交。 ``` 如图 4 所示,三条铁律组合起来,形成一个防翻车的工程闭环: ```mermaid flowchart TD A["你提出需求/意图"] --> B["AI 输出变更方案 Spec"] B --> C{"你审核方案"} C -- 不通过 --> B C -- 通过 --> D["AI 在限定范围内写代码"] D --> E["AI 自行运行测试"] E --> F{"测试全通过?"} F -- 否 --> D F -- 是 --> G["你验收最终结果"] G --> H["合并到主分支"] ``` 人机分工很清楚:你负责定方向、审方案、验收结果,AI 负责出方案、写代码、跑测试。 --- ## 四、直接抄作业的 Rules 模板 下面这份 Rules 模板是我自己在多个项目里打磨后的版本,可以直接复制到项目根目录的 `.cursorrules`、`CLAUDE.md` 或 `RULES.md` 文件里。 ```markdown # 项目 AI 编程规范 ## 核心原则 - 每次修改前先输出中文变更方案(Spec),包含:修改文件列表、逻辑说明、接口变更、潜在风险。等待确认后再写代码。 - 只修改明确指定的文件,不擅自新建文件、不引入新依赖、不重构未被提及的模块。 - 如需新增文件或依赖,先说明理由并等待批准。 ## 代码风格 - 遵循项目已有的命名规范和目录结构,不擅自变更。 - 中文注释,简洁明了,不写废话注释(如 // 获取用户列表 → getUserList())。 - 不做过度抽象,不为"可能的未来需求"提前设计。 ## 验证要求 - 完成修改后必须运行以下命令,全部通过后再提交: - 类型检查(如 tsc --noEmit / mypy) - 单元测试(如 npm test / pytest) - 构建验证(如 npm run build / go build) - 任何一项未通过,自行修复后重新运行。 ## 禁止事项 - 禁止 Accept All,每次修改必须可解释。 - 禁止在未经确认的前提下修改数据库 schema、API 接口签名或核心配置文件。 - 禁止删除或注释掉已有的测试用例。 ``` 简单解读几条: "等待确认后再写代码":对应铁律 2,防止 AI 一上来就闷头写,方向错了全白费。 "不做过度抽象":AI 特别喜欢搞 Factory Pattern、Strategy Pattern 这类设计模式,你只是做个简单功能,它能给你造出五层抽象。这条直接掐住。 "禁止删除已有的测试用例":血泪教训。AI 在修 Bug 时发现测试不通过,它的解法有时候不是修代码,而是把测试删了。你不写这条规则,迟早遇上。 --- ## 五、AI 时代的真功夫 回到开头的问题。Vibe Coding 到底有没有用? 有用。验证想法、做原型、探索可行性,它依然是最快的方式。一个周末用 AI 搓出一个能跑的 Demo,看看这个想法值不值得认真做,完全没问题。 但当你决定"这个东西要认真做"的时候,你得换一套工作方式,从无约束的 Vibe Coding 切换到有规范的 Spec-driven 模式。 你去看那些真正能独立做出产品、持续交付、靠此盈利的开发者,他们用 AI 的时间可能比你还多,区别在于他们知道什么时候该让 AI 飞,什么时候该把缰绳拉回来。 享受 Vibe Coding 带来的灵感和速度。但当你要交付产品的时候,带上那 3 条铁律。它们不会让你变慢,但能让你不翻车。 --- > 我是不会喷火的小火龙。在这里,我不聊虚头巴脑的 AI 概念,只记录一个真实开发者用 AI 搓工具、做产品、踩坑填坑的全过程。如果你也想用 AI 做出真正能稳定上线的小产品,欢迎关注我的公众号「小火龙AI 手记」。少走一点弯路,我们一起把 AI 驯化成趁手的生产力工具。
