编程导航算法话题讨论

算法

375 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

AI全栈或者普通全栈还考不考算法?-AI时代算法考察权重变化

### 求职目标 现在在找AI全栈岗 ### 个人情况 3年经验,前端、Java后端,Python工程化都已掌握并能独立开发项目部署上线。 ### 求职困惑 想知道现在全栈岗的面试中算法考察的比例怎么样,还考不考算法了。特别是中大厂。想了解一下情况好分配准备面试的权重。 ### 期望帮助 最好能有真实的面试案例分享,说说现在AI全栈岗面试的侧重点

卷AI、卷算法、2026 年的前端工程师到底在卷什么?

最近是 2026 年的春招季,前几周密集面了大概快二十个前端。 翻开这批简历,我有一种极其魔幻的感觉:满屏都是 AI,满屏都是算法。 四五年前,大家简历上的高频词还是 精通 Vue3 响应式原理、熟练掌握 Webpack 性能调优。 现在呢?十个候选人里,有九个写着熟练掌握 LLM 接入、深入理解 RAG(检索增强生成)、精通 Prompt 工程、参与过大模型 Agent 平台建设,剩下那个没写 AI 的,简历里赫然写着LeetCode 刷题 150+,精通动态规划与图论。 前端这个圈子,仿佛在一夜之间得了严重的技术焦虑并发症。大家都在拼命往简历里塞最高大上的词,生怕在 2026 年这个节点,因为不懂 AI 而被直接淘汰。 ## 但现实是什么? 上周我淘汰了一个简历写得极其华丽、号称 主导过公司核心 AI 助手前端架构 的候选人。 我没问他大模型底层原理,也没让他手撕红黑树,我只问了他一个极其真实的业务场景: 在一个 AI 流式输出(Streaming)的对话场景里,如果大模型返回的是一个极其复杂的、带有代码块和多步工具调用(Tool Call)的 JSON 块。在流式传输还没结束、JSON 还是残缺状态的时候,你的前端是怎么保证 UI 不崩溃,并且能平滑渲染中间状态的? 他愣了半分钟,支支吾吾地说:我们用的是 Vercel AI SDK,它内部封装好了,直接拿 useChat 里的 messages 渲染就行…… 😖😖😖... 我叹了口气,在面试评价上默默写下:只会调用 API,缺乏处理复杂工程能力。 这就是 2026 年前端圈最大的悲哀:大家都在卷 AI,但 90% 的人卷的只是如何发送一个带 API Key 的 HTTP 请求。 ## 别把调用 API 包装成核心竞争力 🤷‍♂️ 现在很多前端对懂 AI的理解极其肤浅。 以为在项目里接个 OpenAI 或者 Claude 的接口,搞个对话框,把输入框的字传过去,把返回的字用 Markdown 渲染出来,就叫AI 前端工程师了😖。 兄弟,那不叫 AI 开发,那叫表单提交。这种活儿,三年前刚培训班毕业的实习生也会干。 大模型时代,前端真正的难点根本不是发送请求,而是 应对大模型带来的复杂性。 以前我们写业务代码,接口返回的数据结构是确定的,是后端的 Swagger 定义好的。你只需要 if (res.code === 200) 然后按部就班地渲染。 但在 2026 年,大模型吐出来的东西是不可控的。 真实的高阶 AI 前端工程,每天要面对的是这些破事: 流式返回进行到一半,JSON 连个闭合的括号都没有,你的界面怎么解析?怎么渲染正在打字的生成式 UI? 一个 Agent 在后台疯狂调用工具(查天气、查数据库、画图),这个过程中产生的大量异步中间状态,如何在 React/Vue 中做防抖、状态合并和打断(Abort)? 大模型突然抽风,返回了完全不符合预期的组件协议,你的前端系统能不能做沙盒隔离,保证不引发整个页面的白屏崩溃? 这些问题,根本不是你背几个 Prompt 模板就能解决的。它考验的是你对数据流处理、AST(抽象语法树)解析、复杂状态机设计以及防御性编程的底层功底。 你卷了半天 Vercel AI SDK 的用法,一旦业务场景超出了 SDK 的默认配置,你立马就抓瞎了。 (顺手推几个技术大厂的机会,前、后端or测试,感兴趣可以[试试 ](https://jsj.top/f/o38ijj) ) ## 为什么面试官越来越爱考算法? 说完了 AI,再聊聊算法。这也是现在前端同行疯狂吐槽的点:我特么一个画页面的,凭什么让我手写动态规划?🤔 其实这是个很残酷的信号。 作为面试官,我跟你交个底:因为那些常规的、套路化的前端业务代码,现在 AI 真的能写了,而且写得比你快。 2026 年了,如果你只会写个增删改查的表格,只会封装个按钮组件,我在面试里连问你的兴趣都没有。既然基础的搬砖工作被 AI 大幅压缩了,那公司招人,过滤标准自然就要往上提。 考算法,本质上考的不是你对某道题的背诵能力,而是考你的复杂逻辑拆解能力和极限思维。 特别是在做 AI 工具链的前端时: 当你要在浏览器端用 WebAssembly 跑一个轻量级的向量数据库(Vector DB)进行本地 RAG 检索时,不懂数据结构你连原理都看不懂。 当你要处理大模型返回的超大文档树,做精确的 DOM 节点比对和替换时,树的遍历算法就是你的基本功。 大家不是在卷算法,而是在抢夺那些AI 无法轻易替代的深水区岗位🤔。 ## 没必要那么焦虑 前天面试结束,跟几个同组的技术老炮抽烟。大家感慨,其实这十年来,前端圈的焦虑从来没停过。 当年 jQuery 被 React 淘汰时,大家在卷;后来小程序大爆发时,大家也在卷;现在大模型来了,大家不过是换了个名词继续卷。 别被那种 AI 要干掉前端的鬼话吓倒了,也别为了迎合面试官去死记硬背什么 RAG 架构图。 潮水退去的时候,企业最终留下的,永远不是那个会背时髦名词的人,而是那个懂 HTTP 协议、懂浏览器底层、能在复杂的异步环境里把一个烂摊子稳稳托住的前端。 在这个越发喧嚣的 2026 年,少去追逐那些虚幻的词汇,多去打磨你手里的基本功吧🤷‍♂️ 共勉🙌 ——转载自:ErpanOmer

LeetCode Hot 100 二分查找篇 - 每次砍一半,六道题从入门到穿透

# LeetCode Hot 100 二分查找篇 - 每次砍一半,六道题从入门到穿透 哈喽,大家好呀,我是蔚蓝~,今天我们来聊 LeetCode Hot 100 的二分查找。 你查过字典吗?不是手机上那种输入法自动补全,是真的翻一本厚厚的纸质字典。 翻到中间,看一下,字母在左边就翻左边,在右边就翻右边。每次把搜索范围砍掉一半。 这就是二分查找。 听起来简单到不值一提对吧?但 Hot 100 里选了六道二分的题,从 Easy 一路杀到 Hard。你会发现,同样是「每次砍一半」,切的姿势可以天差地别。 今天我把这六道题拆开,从标准模板到双数组切割,逐级通关。 ## 一、LC35 搜索插入位置 - 先把标准模板焊死 这是二分查找的「Hello World」。 给一个有序数组和一个目标值,找到了返回下标,找不到就返回它该插入的位置。 核心问题来了:**找不到的时候,循环结束那一刻,`l` 和 `r` 分别指向哪里?** ![file-20260419121342806.png](https://pic.code-nav.cn/post_picture/1730460161342042114/b8rTHwPrje37KVYD.webp) `l` 指向第一个大于等于 target 的位置,`r` 指向最后一个小于 target 的位置。所以 `l` 刚好就是插入点。 这不是巧合。是循环不变量决定的 —— 整个过程里,`l` 左边的元素始终小于 target,`r` 右边的元素始终大于 target。当 `l > r` 的时候,`l` 就是那个分界线。 ```java class Solution { public int searchInsert(int[] nums, int target) { int l = 0, r = nums.length - 1; while (l <= r) { int mid = l + (r - l) / 2; // 防溢出,等价于 (l+r)/2 if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { l = mid + 1; } else { r = mid - 1; } } return l; // l 就是第一个 >= target 的位置 } } ``` **避坑:** - 不要用 `(l + r) / 2`,数据量大了会溢出 - 循环条件是 `l <= r`,不是 `l < r`,否则会漏检 - 找不到时返回 `l`,不是 `r`,也不是 `-1` 这三个坑,你踩过几个?我当年三个全踩了。 --- ## 二、LC74 搜索二维矩阵 - 展平了就是一维 给你一个矩阵,每行递增,每行第一个数比上一行最后一个数大。问 target 在不在里面。 看到「每行第一个数比上一行最后一个数大」这个条件,你就该反应过来:**按行展开,这不就是一个严格递增的一维数组吗?** 把二维坐标映射成一维下标:`row = idx / n, col = idx % n`。然后就是标准的二分查找。 ```java class Solution { public boolean searchMatrix(int[][] matrix, int target) { int R = matrix.length, C = matrix[0].length; int l = 0, r = R * C - 1; while (l <= r) { int mid = l + (r - l) / 2; int val = matrix[mid / C][mid % C]; // 一维下标映射到二维坐标 if (val == target) { return true; } else if (val < target) { l = mid + 1; } else { r = mid - 1; } } return false; } } ``` ![file-20260419121647916.png](https://pic.code-nav.cn/post_picture/1730460161342042114/JFEvgQ0ZA1jePQ3r.webp) **一眼识别:** 只要看到「每行首元素大于上一行尾元素」,立刻想到展平。不用真的开新数组,坐标映射就够了,O(1) 空间。 --- ## 三、LC34 查找首尾位置 - 一个 lower_bound 走天下 给一个非递减数组,可能有重复元素,找某个值的第一个和最后一个位置。 如果用两次二分,分别找左边界和右边界,代码写起来会重复。这题有一个很优雅的 trick: **右边界 = `lower_bound(target + 1) - 1`** 什么意思?`lower_bound(x)` 找到第一个大于等于 `x` 的位置。那么 `lower_bound(target + 1)` 就是第一个大于 target 的位置,再减一,就是最后一个等于 target 的位置。 ![file-20260419121846724.png](https://pic.code-nav.cn/post_picture/1730460161342042114/jKKfZlCp0Qs0QkT4.webp) 一个函数,两次调用,两个边界全搞定。 ```java class Solution { public int[] searchRange(int[] nums, int target) { if (nums.length == 0) return new int[]{-1, -1}; int[] ans = new int[2]; ans[0] = lowerBound(nums, target); // 第一个 >= target ans[1] = lowerBound(nums, target + 1) - 1; // 最后一个 <= target if (ans[0] >= nums.length || nums[ans[0]] != target) { return new int[]{-1, -1}; // target 不存在 } return ans; } // 返回第一个 >= target 的位置 int lowerBound(int[] nums, int target) { int l = 0, r = nums.length - 1; while (l <= r) { int mid = l + (r - l) / 2; if (nums[mid] < target) { l = mid + 1; } else { r = mid - 1; // >= 的情况,mid 可能是答案,但左边可能还有更小的 } } return l; } } ``` **避坑:** 找到左边界后,必须检查 `nums[ans[0]] == target`。万一 target 比所有元素都大,`lowerBound` 返回的是 `nums.length`,直接越界。 --- ## 四、LC33 搜索旋转排序数组 - 必有一半有序 数组被旋转过了,比如 `[0,1,2,4,5,6,7]` 变成了 `[4,5,6,7,0,1,2]`。要在里面找 target。 乍一看完全无序,没法二分? 但你仔细看,不管从哪里切一刀,**左右两半中必定有一半是有序的**。 比如 `mid = 3`,`nums = [4,5,6,7,0,1,2]`,左半段 `[4,5,6,7]` 是有序的。那就先判断 target 是不是在这个有序区间里。在的话就在这个区间继续二分,不在就去另一半。 ![file-20260419121939652.png](https://pic.code-nav.cn/post_picture/1730460161342042114/t6DbzqNm0Pon3BO7.webp) ```java class Solution { public int search(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) return mid; if (nums[left] <= nums[mid]) { // 左半段有序 if (nums[left] <= target && target < nums[mid]) { right = mid - 1; // target 在左半段 } else { left = mid + 1; // target 在右半段 } } else { // 右半段有序 if (nums[mid] < target && target <= nums[right]) { left = mid + 1; // target 在右半段 } else { right = mid - 1; // target 在左半段 } } } return -1; } } ``` **避坑:** `nums[left] <= nums[mid]` 这里必须用 `<=`,不能用 `<`。当 `left == mid` 时(区间只有一两个元素),左半段也是「有序」的。用 `<` 会漏掉。 这个判断是这题唯一的难点。想通了,整题就通了。 --- ## 五、LC153 寻找旋转最小值 - 和右边比,不和左边比 同样是旋转数组,这回不找 target,找最小值。 最小值就在「旋转点」的位置 —— 升序被打断的地方。比如 `[4,5,6,7,0,1,2]`,旋转点在 `7` 和 `0` 之间,最小值是 `0`。 ![file-20260419122134606.png](https://pic.code-nav.cn/post_picture/1730460161342042114/sc1PwAF1RQrlICqS.webp) 怎么找?拿 `nums[mid]` 和 `nums[right]` 比: - `nums[mid] > nums[right]`:mid 在旋转点左侧,最小值在右边,`left = mid + 1` - `nums[mid] <= nums[right]`:mid 在旋转点右侧(或就是旋转点本身),`right = mid` 为什么和 `nums[right]` 比而不是 `nums[left]`? 因为和 `nums[left]` 比有个致命问题:当数组完全有序没旋转时,`nums[mid] > nums[left]` 恒成立,你分不清「左半段有序」和「整个数组有序」。而和 `nums[right]` 比没有这个歧义。 ```java class Solution { public int findMin(int[] nums) { int left = 0, right = nums.length - 1; while (left < right) { // 注意:< 不是 <= int mid = left + (right - left) / 2; if (nums[mid] > nums[right]) { left = mid + 1; } else { right = mid; // mid 本身可能就是最小值 } } return nums[left]; } } ``` **避坑:** - 循环条件 `left < right`,不是 `<=`。因为 `right = mid` 不是 `mid - 1`,用 `<=` 会死循环 - `right = mid` 不是 `mid - 1`,因为 mid 可能就是答案,不能把它跳过去 这两点和 LC33 完全相反。LC33 是「找到了就排除」,这题是「找到了就留着」。搞混了就完蛋。 --- ## 六、LC4 寻找两个正序数组的中位数 - 在两个数组上各切一刀 二分查找的最终 Boss。 两个有序数组,找中位数,要求 O(log(m+n))。 合并再找?O(m+n),太慢了。 思路是这样的:中位数的本质是把所有元素分成两半,左半的最大值小于右半的最小值。那我在两个数组上各切一刀: ``` nums1: [... 左半 | 右半 ...] nums2: [... 左半 | 右半 ...] ``` 切完之后,如果满足 `nums1 左半最大 <= nums2 右半最小` 且 `nums2 左半最大 <= nums1 右半最小`,这条分割线就找对了。 ![file-20260419122316501.png](https://pic.code-nav.cn/post_picture/1730460161342042114/sDDBSGHaAv95oDqZ.webp) 对较短的数组做二分,枚举分割位置 `i`,由 `j = (m+n+1)/2 - i` 自动确定另一个数组的分割位置。为什么要在短数组上二分?因为如果 `i` 太大,`j` 会变成负数。短数组保证了 `i` 的范围足够小,`j` 始终合法。 ```java class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { // 在短数组上二分,防止 j 越界 if (nums1.length > nums2.length) { return findMedianSortedArrays(nums2, nums1); } int m = nums1.length, n = nums2.length; int left = 0, right = m; while (left <= right) { int i = (left + right) / 2; // nums1 分割位置 int j = (m + n + 1) / 2 - i; // nums2 分割位置 // 边界处理:分割线贴边时用极值兜底 int maxLeft1 = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1]; int minRight1 = (i == m) ? Integer.MAX_VALUE : nums1[i]; int maxLeft2 = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1]; int minRight2 = (j == n) ? Integer.MAX_VALUE : nums2[j]; if (maxLeft1 <= minRight2 && maxLeft2 <= minRight1) { // 分割线找对了 if ((m + n) % 2 == 0) { return (Math.max(maxLeft1, maxLeft2) + Math.min(minRight1, minRight2)) / 2.0; } else { return Math.max(maxLeft1, maxLeft2); } } else if (maxLeft1 > minRight2) { right = i - 1; // nums1 切太靠右了 } else { left = i + 1; // nums1 切太靠左了 } } throw new IllegalArgumentException(); } } ``` **避坑:** - 必须在较短数组上二分,否则 `j` 可能为负 - 边界用 `MIN_VALUE` 和 `MAX_VALUE` 兜底,`i=0` 或 `i=m` 时数组的一侧是空的 - `j = (m+n+1)/2` 里的 `+1` 是为了让奇数个时左半多一个元素,中位数直接取 `max(左半最大)` 就行 这题难的不是二分本身,而是把「中位数」这个概念转化成「分割线」的思路。想通了这个映射,代码反而比前面几道题还清晰。 --- ## 碎碎念 二分查找的代码量都不大,每道题也就十几二十行。但魔鬼全在细节里 —— 循环条件是 `<` 还是 `<=`,`right` 是 `mid` 还是 `mid - 1`,和左边比还是和右边比。 这些细节不是靠背的。你得理解每一行代码背后的不变量是什么,循环退出的时候各个变量指向哪里。 这六道题串起来的线索就一条:**在正确的位置切一刀**。 LC35 在有序数组上切,LC74 把二维展平了切,LC34 用 lower_bound 切两次,LC33 和 LC153 在旋转数组上找对了方向再切,LC4 在两个数组上同时切。 切的依据不同,但刀法一样 —— 每次砍掉一半,O(log n) 收工。 下次面试遇到二分,别慌,先想清楚这一刀该切在哪。

深度解析 Claude Code 记忆系统:从源码看 AI 编程助手如何"记住"你的项目

# 深度解析 Claude Code 记忆系统:从源码看 AI 编程助手如何"记住"你的项目 > 本文基于 Claude Code v2.1.88 泄露源码及社区分析资料,深入剖析其记忆系统的架构设计与实现细节。如果你正在构建 AI Agent 系统,这套记忆架构非常值得参考。 ## 一、为什么需要记忆系统? Claude Code 是 Anthropic 官方的 CLI 编程助手。每次启动一个新会话,模型面对的是一个全新的上下文窗口——它不知道你的项目用什么技术栈、你偏好什么代码风格、上次修了什么 bug。 没有记忆系统,你每次都要花几分钟"教育"AI:解释架构、重复约定、描述上下文。这在复杂项目上是巨大的效率损耗和 token 浪费。 Claude Code 的解决方案是一套 **三层持久化记忆体系**,配合会话内的 **上下文压缩机制**,让 AI 在跨会话和长会话两个维度上都能高效利用知识。 ## 二、记忆系统全景架构 从源码结构看,Claude Code 的记忆相关代码分布在以下目录: ``` src/ ├── memdir/ # Auto Memory 核心(记忆目录管理) │ └── memdir.ts # buildMemoryPrompt / loadMemoryPrompt / 截断逻辑 ├── services/ │ ├── SessionMemory/ # 会话记忆提取(Session Memory) │ │ └── sessionMemory.ts │ └── compact/ # 上下文压缩(三层策略) ├── utils/ │ └── api.ts # CLAUDE.md 注入逻辑(prependUserContext) └── constants/ # System Prompt 各段落定义 ``` 整个记忆体系可以概括为 **"手动 + 自动 + 会话"三层架构**: ``` ┌─────────────────────────────────────────────────┐ │ Layer 1: CLAUDE.md(手动记忆) │ │ 用户手写的持久化指令,每次会话自动加载 │ │ 注入位置:user context(第一条用户消息前) │ ├─────────────────────────────────────────────────┤ │ Layer 2: Auto Memory(自动记忆) │ │ Claude 自主决定写入的跨会话知识 │ │ 存储位置:~/.claude/projects/<project>/memory/ │ │ 注入位置:system prompt 动态段 │ ├─────────────────────────────────────────────────┤ │ Layer 3: Session Memory(会话记忆) │ │ 当前会话内的上下文管理与压缩 │ │ 机制:autoCompact + snipCompact + contextCollapse│ └─────────────────────────────────────────────────┘ ``` ## 三、Layer 1:CLAUDE.md —— 手动记忆层 ### 3.1 不在 System Prompt 里 这是最大的认知误区。很多人以为 CLAUDE.md 的内容被拼接到 system prompt 中,实际上并非如此。 从源码看,CLAUDE.md 通过 `prependUserContext()` 函数注入,插入位置是消息列表的最前面,作为一条特殊的 user message: ```typescript // src/utils/api.ts(简化) export function prependUserContext(messages, context): Message[] { return [ createUserMessage({ content: `As you answer the user's questions, you can use... ${context.claudeMdContent} ${context.memoryContent} ...` }), ...messages ] } ``` 这意味着 CLAUDE.md **不享受 system prompt 级别的缓存优化**。每次会话它作为 user message 的一部分被发送,会消耗实际的输入 token。 ### 3.2 四级作用域 CLAUDE.md 支持四个作用域层级,按优先级从低到高: | 层级 | 路径 | 场景 | |------|------|------| | 企业级 | MDM 管控策略注入 | 企业统一管控 | | 用户级 | `~/.claude/CLAUDE.md` | 个人全局偏好 | | 项目级 | `./CLAUDE.md` 或 `./.claude/CLAUDE.md` | 团队共享约定(提交到 git) | | 本地级 | `./CLAUDE.local.md` | 个人项目偏好(gitignore) | 项目级的 CLAUDE.md 可以通过 `/init` 命令自动生成——Claude 会分析你的代码库,提取构建命令、测试指令和项目约定。 ### 3.3 最佳实践 由于 CLAUDE.md 直接消耗 token,写法上需要注意效率: - **越短越好**:简洁的指令比冗长的描述更容易被模型遵循 - **结构化**:用清晰的分类(构建命令、代码风格、架构约定)组织内容 - **避免废话**:不要写"请帮我"、"你应该"这类无效前缀 - **可执行**:写成可直接执行的命令或可直接遵循的规则 ## 四、Layer 2:Auto Memory —— 自动记忆层(memdir) 这是 Claude Code 记忆系统中最有技术含量的部分,代码位于 `src/memdir/memdir.ts`。 ### 4.1 核心设计:基于文件的记忆目录 每个项目在 `~/.claude/projects/<project>/memory/` 下有一个专属的记忆目录。目录路径通过 git 仓库路径派生,所以同一仓库的所有 worktree 和子目录共享同一个记忆空间。 ``` ~/.claude/projects/ ├── <project-hash-A>/ │ └── memory/ │ ├── MEMORY.md # 索引文件(入口点) │ ├── debugging.md # 主题文件:调试经验 │ ├── patterns.md # 主题文件:代码模式 │ └── architecture.md # 主题文件:架构笔记 └── <project-hash-B>/ └── memory/ └── MEMORY.md ``` ### 4.2 MEMORY.md:200 行硬截断的索引文件 源码中定义了明确的截断常量: ```typescript // src/memdir/memdir.ts export const ENTRYPOINT_NAME = 'MEMORY.md' export const MAX_ENTRYPOINT_LINES = 200 export const MAX_ENTRYPOINT_BYTES = 25_000 // 25KB ``` **只有 MEMORY.md 的前 200 行或前 25KB 会在会话启动时自动加载**。超出部分直接被截断,并且会产生警告提示: ``` WARNING: MEMORY.md is 350 lines (limit: 200). Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files. ``` 这意味着 MEMORY.md 应该被当作**索引**使用,而不是存放大量细节。每条记忆控制在一行、200 字符以内,详细内容放到独立的 topic 文件中。 ### 4.3 Topic 文件:按需加载 像 `debugging.md`、`patterns.md` 这样的主题文件**不会在启动时加载**。Claude 在会话过程中需要相关信息时,会通过标准的文件读取工具按需读取它们。 这是一个典型的冷热分层设计: - **热数据**(MEMORY.md 前 200 行)→ 每次启动加载,消耗固定 token - **温数据**(topic 文件)→ 按需读取,用工具调用触发 - **冷数据**(历史会话)→ 不加载,依赖 Session Memory 提取 ### 4.4 四类记忆分类法 从 `buildMemoryLines()` 函数的源码注释可以看到,Auto Memory 采用了一个封闭的四类分类体系: ```typescript // src/memdir/memdir.ts /** * Constrains memories to a closed four-type taxonomy: * - user: 用户偏好和习惯 * - feedback: 用户的修正和反馈 * - project: 项目特定知识 * - reference: 参考信息 * * Content derivable from current project state (code patterns, * architecture, git history) is explicitly excluded. */ ``` 关键设计原则:**可以从代码库当前状态推导出来的信息(代码模式、架构、git 历史)被显式排除在记忆之外**。这避免了记忆与实际代码之间的不一致——代码变了但记忆没更新的问题。 ### 4.5 记忆写入时机 Claude 并不是每次会话都写记忆。从源码和官方文档看,它的写入策略是: 1. **用户纠正时**:当用户纠正 Claude 的行为("不要用分号"、"我们用 pnpm 不用 npm"),Claude 会主动将这些偏好写入记忆 2. **有价值的发现时**:调试过程中发现的坑、构建系统的特殊配置等 3. **Claude 自主判断**:基于"这条信息在未来的对话中是否有用"来决定是否记忆 写入操作在 UI 中会显示 "Writing memory" 提示,用户可以随时编辑或删除这些文件(它们就是普通的 Markdown 文件)。 ### 4.6 记忆注入到 System Prompt `loadMemoryPrompt()` 函数负责将记忆内容注入到 system prompt 的动态部分(`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之后): ```typescript // src/memdir/memdir.ts(简化) export function buildMemoryPrompt(params: { displayName: string memoryDir: string extraGuidelines?: string[] }): string { const entrypoint = params.memoryDir + ENTRYPOINT_NAME const raw = fs.readFileSync(entrypoint, { encoding: 'utf-8' }) const t = truncateEntrypointContent(raw) // 硬截断 const lines = buildMemoryLines(params.displayName, params.memoryDir, ...) lines.push(`## ${ENTRYPOINT_NAME}`, '', t.content) return lines.join('\n') } ``` 注意这里是**同步读取**(`fs.readFileSync`),因为记忆加载发生在 system prompt 组装阶段,需要在 API 调用前完成。 ## 五、Layer 3:Session Memory —— 会话内记忆管理 ### 5.1 Session Memory 提取 `src/services/SessionMemory/sessionMemory.ts` 实现了从对话过程中自动提取值得记忆的内容。从架构分析中可以看到,它的工作方式是: ```typescript // 会话结束或 compact 触发时 shouldExtractMemory() -> runForkedAgent() -> Session Memory 更新 ``` 这里用了一个巧妙的设计——**fork 出一个子 agent 来做记忆提取**,不阻塞主会话的执行流程。 ### 5.2 三层上下文压缩 长会话中上下文会逐渐填满,Claude Code 的 `services/compact/` 实现了三层压缩策略: | 策略 | 机制 | 触发时机 | |------|------|----------| | autoCompact | 将历史消息总结为摘要 | 上下文接近限制时自动触发 | | snipCompact | 修剪冗长的工具输出 | 单次工具输出过大时 | | contextCollapse | 折叠旧的消息段落 | 保留关键信息,折叠细节 | 这套压缩机制确保了即使在超长会话中,关键的上下文信息不会因为窗口限制而丢失。 ## 六、System Prompt 的缓存架构 Claude Code 的 system prompt 不是一整块文本,而是被精心分割为**静态段**和**动态段**: ``` System Prompt 结构: ┌──────────────────────────────────┐ │ 静态段(可缓存) │ ← Anthropic API prompt caching │ - 角色定义 │ │ - 工具定义(schema) │ │ - 安全规则 │ │ - 基础指令 │ ├──── SYSTEM_PROMPT_DYNAMIC_BOUNDARY ──┤ │ 动态段(每轮重算,破坏缓存) │ ← DANGEROUS_uncached │ - Auto Memory 内容 │ │ - MCP 工具列表 │ │ - 当前项目上下文 │ │ - 功能开关状态 │ └──────────────────────────────────┘ ``` 静态段通过 Anthropic API 的 prompt caching 机制可以被缓存复用,大幅降低 token 消耗和延迟。Auto Memory 注入在动态段——这意味着**每次记忆内容变化,都会破坏后续的缓存**,所以记忆内容要尽量稳定和精简。 ## 七、CLAUDE.md vs Auto Memory:注入位置的差异 这两套机制虽然都提供跨会话记忆,但注入位置完全不同: | 维度 | CLAUDE.md | Auto Memory | |------|-----------|-------------| | 注入位置 | User message(对话开头) | System prompt(动态段) | | 缓存影响 | 不影响 system prompt 缓存 | 变化会破坏缓存 | | 加载方式 | 完整加载(无截断) | MEMORY.md 前 200 行 | | 写入者 | 用户手动 | Claude 自动 | | 版本控制 | 可提交到 git | 本地存储,不共享 | | 适合内容 | 团队约定、构建命令 | 个人偏好、调试经验 | ## 八、记忆系统的设计启示 如果你在构建自己的 AI Agent 记忆系统,Claude Code 的设计有几个值得借鉴的点: ### 8.1 分层而非全量 不是所有记忆都需要每次加载。热数据(MEMORY.md 索引)自动加载,温数据(topic 文件)按需读取,冷数据(历史会话)通过提取机制保留精华。 ### 8.2 索引 + 详情分离 MEMORY.md 当索引用,详细内容放 topic 文件。这个模式和数据库的"索引-数据"分离思想一致——索引小且快,数据大但不常读。 ### 8.3 排除可推导信息 不记忆可以从代码库当前状态推导出来的内容。这避免了记忆与现实不一致的维护负担。 ### 8.4 记忆是纯文本 所有记忆都是 Markdown 文件,人类可读可编辑。没有私有数据库、没有向量存储、没有嵌入计算。简单可靠。 ### 8.5 硬性截断保护 200 行 / 25KB 的硬截断不是建议,是强制执行的。这种"宁可丢信息也不爆上下文"的设计,在生产系统中非常重要。 ## 九、局限性 当然,这套系统也有明显的局限: - **单 Agent 绑定**:记忆存储在 `~/.claude/` 下,不能跨工具共享(比如 Cursor 或 OpenCode 无法读取) - **无语义检索**:topic 文件的查找依赖 Claude 自己的文件读取工具,本质上是 grep 级别的搜索,没有向量相似度匹配 - **无版本管理**:记忆文件没有版本历史,误删除或错误修改无法回退 - **上下文开销**:即使是索引级别的记忆,200 行也是不小的 token 消耗 社区已经有一些项目在尝试解决这些问题,比如 memsearch(跨 Agent 语义记忆)和 memory-mcp(基于 MCP 的记忆服务),有兴趣的可以关注。 ## 十、总结 Claude Code 的记忆系统是一个**务实的工程设计**——没有花哨的向量数据库,没有复杂的 RAG 管线,就是 Markdown 文件 + 分层加载 + 硬性截断。但这种简单性恰恰是它的优势:可预测、可调试、可人工干预。 对于 AI Agent 开发者来说,Claude Code 的记忆架构提供了一个很好的参考基线:先用最简单的方案解决 80% 的问题,再在必要时引入更复杂的机制。 --- **参考资料:** - [Claude Code 源码教学指南](https://zhu1090093659.github.io/claude-code-cookbook/books/) - 中英双语 20 章教程 - [Claude Code 官方记忆文档](https://code.claude.com/docs/en/memory) - [liuup/claude-code-analysis](https://github.com/liuup/claude-code-analysis) - 源码静态分析 - [Claude Code Architecture Breakdown](https://ccleaks.com/architecture) - [深入浅出 Claude Code:从源码理解 CLAUDE.md](https://linux.do/t/topic/1871216) > 如果这篇文章对你有帮助,欢迎点赞收藏。关于 AI Agent 架构设计的更多内容,可以关注我后续的分享。

Claude Code 源码架构深度解析:1884 个文件背后的 AI 编程工具设计哲学

# Claude Code 源码架构深度解析:1884 个文件背后的 AI 编程工具设计哲学 > 近日 Claude Code 源码意外泄漏,作为一个深度使用 Claude Code 的开发者,我对这份 33MB、1884 个 TypeScript 文件的代码库做了一次完整的架构分析。本文将从宏观到微观,拆解这个当前最强 AI 编程助手的内部设计。 ## 一、项目概览 | 指标 | 数值 | | ----------- | --------------------------------------------- | | 文件总数 | 1,884 个 (.ts/.tsx) | | 代码体积 | 33 MB | | 核心入口文件 | main.tsx (4,683 行) | | 查询引擎 | QueryEngine.ts (1,295 行) + query.ts (1,729 行) | | 内置工具 | 43 个独立模块 | | 斜杠命令 | 100+ 个 | | React Hooks | 85 个 | | 服务模块 | 35+ 个 | | 技术栈 | TypeScript + React + Ink (终端 UI) + Zod + Bun | *** ## 二、整体架构:五层设计 先上全局架构图,后面逐层拆解: ![architecture_overview.svg](https://pic.code-nav.cn/post_picture/1663738971949125633/2PRGZeNtHKPVnX5L.svg) ![startup_flow.svg](https://pic.code-nav.cn/post_picture/1663738971949125633/yYyj3plyU1GFgHFN.svg) ![query_dataflow.svg](https://pic.code-nav.cn/post_picture/1663738971949125633/6AF5lfhSyqi5iNgB.svg) ## 三、入口层:毫秒级的启动优化 ### 3.1 快速路径(Fast Path) `cli.tsx` 是整个应用的入口,只有 302 行,但信息密度极高。 ```typescript // cli.tsx — 第一行就是 feature flag import { feature } from 'bun:bundle'; // 顶层副作用:环境准备 process.env.COREPACK_ENABLE_AUTO_PIN = '0'; // 防止 corepack 污染 package.json // CCR 远程环境设置 8GB 堆上限 if (process.env.CLAUDE_CODE_REMOTE === 'true') { process.env.NODE_OPTIONS = existing ? `${existing} --max-old-space-size=8192` : '--max-old-space-size=8192'; } ``` 关键设计:**所有 import 都是动态的**。`--version` 路径零模块加载: ```typescript async function main(): Promise<void> { const args = process.argv.slice(2); // 快速路径 1: --version — 零 import,直接输出 if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) { console.log(`${MACRO.VERSION} (Claude Code)`); // 编译时内联 return; } // 快速路径 2: --dump-system-prompt (Ant-only, 编译时消除) if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') { ... } // 快速路径 3: Chrome 原生宿主 if (process.argv[2] === '--chrome-native-host') { ... } // 快速路径 4: Daemon worker (内部 supervisor 用) if (feature('DAEMON') && args[0] === '--daemon-worker') { ... } // 快速路径 5: Bridge 远程控制 if (feature('BRIDGE_MODE') && (args[0] === 'remote-control' || args[0] === 'rc')) { ... } } ``` 每个分支都有 `profileCheckpoint()` 调用,方便 Anthropic 内部做启动性能分析。 ### 3.2 反调试机制 外部构建包含一个反调试检查,检测到 `--inspect` 或 Node Inspector 活跃时直接退出: ```typescript // 外部构建检测调试器 if ("external" !== 'ant' && isBeingDebugged()) { process.exit(1); // 静默退出,无错误信息 } ``` 这里 `"external"` 是编译时替换的字符串——Anthropic 内部构建替换为 `'ant'`,外部构建保持 `'external'`。所以只有外部用户会被拦截,内部开发者不受影响。 ### 3.3 main.tsx:4683 行的超级编排器 `main.tsx` 是我见过最复杂的单文件初始化逻辑。它的前 20 行就包含了三个并行的副作用: ```typescript // 第 1 行:标记入口时间戳 import { profileCheckpoint } from './utils/startupProfiler.js'; profileCheckpoint('main_tsx_entry'); // 第 2 行:火速启动 MDM 子进程(macOS plutil / Windows reg query) import { startMdmRawRead } from './utils/settings/mdm/rawRead.js'; startMdmRawRead(); // 第 3 行:并行读取 macOS Keychain(OAuth token + 旧版 API key) import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js'; startKeychainPrefetch(); ``` 这三个操作在模块加载阶段(`import` 副作用)就开始执行,比 `main()` 函数早 135ms 以上。这是因为后续 135ms 花在加载其余 100+ 个 import 上——这段时间内 MDM 和 Keychain 的子进程已经在后台跑完了。 ### 3.4 迁移系统 `main.tsx` 包含一个版本化的迁移系统,当前版本号是 11: ```typescript const CURRENT_MIGRATION_VERSION = 11; function runMigrations(): void { if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) { migrateAutoUpdatesToSettings(); migrateSonnet45ToSonnet46(); // 模型名升级 migrateOpusToOpus1m(); // Opus → Opus 1M migrateBypassPermissionsAcceptedToSettings(); // 权限状态迁移 // ... 更多迁移 saveGlobalConfig(prev => ({ ...prev, migrationVersion: CURRENT_MIGRATION_VERSION })); } } ``` 每次版本号变更时,所有迁移函数幂等执行——已经迁移过的会 early return。 ### 3.5 REPL 启动 最终通过 `launchRepl()` 启动交互界面: ```typescript // replLauncher.tsx — 动态 import App 和 REPL export async function launchRepl(root, appProps, replProps, renderAndRun) { const { App } = await import('./components/App.js'); const { REPL } = await import('./screens/REPL.js'); await renderAndRun(root, <App {...appProps}><REPL {...replProps} /></App>); } ``` `App` 组件包裹了 `ThemeProvider`(在 `ink.ts` 中注入),提供亮/暗主题支持。Ink 本身是主题无关的——Claude Code 通过全局包装层注入主题。 *** ## 四、状态管理:34 行的极简 Store 整个应用的状态管理只有 34 行代码,**不依赖任何第三方库**: ```typescript // state/store.ts — 完整源码 type Listener = () => void type OnChange<T> = (args: { newState: T; oldState: T }) => void export type Store<T> = { getState: () => T setState: (updater: (prev: T) => T) => void subscribe: (listener: Listener) => () => void } export function createStore<T>( initialState: T, onChange?: OnChange<T>, ): Store<T> { let state = initialState const listeners = new Set<Listener>() return { getState: () => state, setState: (updater: (prev: T) => T) => { const prev = state const next = updater(prev) if (Object.is(next, prev)) return // 核心:引用相等检查 state = next onChange?.({ newState: next, oldState: prev }) for (const listener of listeners) listener() }, subscribe: (listener: Listener) => { listeners.add(listener) return () => listeners.delete(listener) }, } } ``` 配合 React 18 的 `useSyncExternalStore` 实现最小化重渲染: ```typescript // state/AppState.tsx export function useAppState<T>(selector: (state: AppState) => T): T { const store = useAppStore(); return useSyncExternalStore( store.subscribe, () => selector(store.getState()), ); } ``` **为什么不用 Zustand 或 Redux?** 因为 Claude Code 的状态更新模式非常简单——没有中间件、没有 devtools、没有异步 action。一个 `Object.is()` + `Set<Listener>` 就够了。34 行代码 vs Zustand 的 1000+ 行,性能更好,依赖更少。 ### AppState 的形状 ```typescript export type AppState = DeepImmutable<{ settings: SettingsJson mainLoopModel: ModelSetting toolPermissionContext: ToolPermissionContext verbose: boolean statusLineText: string | undefined isBriefOnly: boolean spinnerTip?: string kairosEnabled: boolean // Bridge/Remote 状态 remoteSessionUrl: string | undefined remoteConnectionStatus: 'connecting' | 'connected' | 'reconnecting' | 'disconnected' replBridgeEnabled: boolean replBridgeConnected: boolean // ... }> & { // 可变部分 tasks: { [taskId: string]: TaskState } agentNameRegistry: Map<string, AgentId> foregroundedTaskId?: string mcp: { clients: MCPServerConnection[] tools: Tool[] commands: Command[] resources: Record<string, ServerResource[]> } } ``` 注意:大部分字段是 `DeepImmutable`(只读递归类型),但 `tasks` 和 `mcp` 是可变的——因为它们需要高频更新。 *** ## 五、查询引擎:AsyncGenerator 驱动的对话循环 ### 5.1 QueryEngine 类 `QueryEngine` 管理一次对话的完整生命周期,核心是一个异步生成器: ```typescript export type QueryEngineConfig = { cwd: string tools: Tools commands: Command[] mcpClients: MCPServerConnection[] agents: AgentDefinition[] canUseTool: CanUseToolFn getAppState: () => AppState setAppState: (f: (prev: AppState) => AppState) => void readFileCache: FileStateCache customSystemPrompt?: string appendSystemPrompt?: string thinkingConfig?: ThinkingConfig maxTurns?: number // SDK 模式下限制轮数 maxBudgetUsd?: number // 成本预算 // ... } export class QueryEngine { private mutableMessages: Message[] = [] // 跨 turn 持久化 private fileStateCache: FileStateCache // 文件读取缓存 private totalUsage: NonNullableUsage // 累计 token private permissionDenials: SDKPermissionDenial[] = [] async *submitMessage( prompt: string | ContentBlockParam[], options?: { uuid?: string; isMeta?: boolean } ): AsyncGenerator<SDKMessage, void, unknown> { // 逐条 yield 消息,实现真正的流式响应 } } ``` ### 5.2 数据流详解 用户输入 "帮我写一个 TODO 应用" │ ▼ processUserInput() │ 解析斜杠命令 (/commit, /review 等) │ 展开粘贴内容 ([Pasted text #1 +10 lines]) │ 处理附件(图片、PDF) ▼ fetchSystemPromptParts() │ ① getSystemContext() — Git状态(memoized, 一次对话只算一次) │ ├─ getBranch() │ ├─ getDefaultBranch() │ ├─ git status --short (截断至2000字符) │ ├─ git log --oneline -n 5 │ └─ git config user.name │ ② getUserContext() — claude.md 发现 + 当前日期 │ ③ getCoordinatorUserContext() — Coordinator 模式专用 │ ④ 技能前端matter + 工具描述 ▼ query() — 核心查询函数 (1729行) │ ├─ normalizeMessagesForAPI() │ 过滤掉 progress/system 消息 │ 将内部消息格式转为 API 格式 │ ├─ queryModelWithStreaming() │ 调用 Claude API (带 prompt cache) │ yield AssistantMessage (流式) │ ├─ 提取 tool_use blocks │ ├─ StreamingToolExecutor.execute() │ ├─ 并发安全工具 → Promise.all() 并行执行 │ └─ 非安全工具 → 顺序执行 │ ├─ applyToolResultBudget() │ 大结果截断,防止上下文膨胀 │ ├─ executePostSamplingHooks() │ Stop hook / PostToolUse hook │ ├─ calculateTokenWarningState() │ 检查是否接近上下文窗口上限 │ └─ autoCompact / snipCompact (如果启用) 压缩旧消息,释放上下文空间 ### 5.3 上下文压缩策略 当对话接近 token 上限时,有三种压缩策略(通过 Feature Gate 控制): ```typescript // services/compact/autoCompact.ts export const AUTOCOMPACT_BUFFER_TOKENS = 13_000 // 触发阈值缓冲 export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // 警告阈值 export function getAutoCompactThreshold(model: string): number { const effectiveContextWindow = getEffectiveContextWindowSize(model) return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS } // 有效上下文窗口 = 总窗口 - 预留输出空间(20000) export function getEffectiveContextWindowSize(model: string): number { const reservedTokensForSummary = Math.min( getMaxOutputTokensForModel(model), 20_000 ) return contextWindow - reservedTokensForSummary } ``` 三种策略: | 策略 | Feature Gate | 说明 | | ---------------- | ------------------ | -------------- | | Auto Compact | 默认启用 | 到达阈值时压缩旧消息为摘要 | | Reactive Compact | `REACTIVE_COMPACT` | 主动式压缩,更激进 | | History Snip | `HISTORY_SNIP` | 直接截断历史,仅保留最近消息 | | Context Collapse | `CONTEXT_COLLAPSE` | 上下文折叠,语义压缩 | *** ## 六、工具系统:43 个模块的插件式架构 ### 6.1 工具注册 `tools.ts` 是工具的注册中心,390 行代码管理所有工具的生命周期: ```typescript // tools.ts — 所有工具的注册 export function getAllBaseTools(): Tools { return [ AgentTool, TaskOutputTool, BashTool, // Ant 内部构建嵌入了 bfs/ugrep,不需要独立的 Glob/Grep ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]), FileReadTool, FileEditTool, FileWriteTool, NotebookEditTool, WebFetchTool, TodoWriteTool, WebSearchTool, SkillTool, // ... 更多工具 // Feature-gated 工具 (编译时消除) ...(WebBrowserTool ? [WebBrowserTool] : []), ...(SleepTool ? [SleepTool] : []), ...(SnipTool ? [SnipTool] : []), ...(WorkflowTool ? [WorkflowTool] : []), // 仅在 Ant 内部构建启用 ...(process.env.USER_TYPE === 'ant' ? [ConfigTool, TungstenTool] : []), ...(REPLTool ? [REPLTool] : []), // 仅在测试环境 ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []), ] } ``` ### 6.2 工具池组装 内置工具和 MCP 工具通过 `assembleToolPool()` 合并: ```typescript export function assembleToolPool( permissionContext: ToolPermissionContext, mcpTools: Tools, ): Tools { const builtInTools = getTools(permissionContext) const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext) // 关键设计:按 name 排序,保证 prompt cache 稳定性 // 如果 MCP 工具插入到内置工具之间,会导致所有下游 cache key 失效 const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name) // uniqBy 保留插入顺序 → 内置工具同名时优先 return uniqBy( [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)), 'name', ) } ``` ### 6.3 Simple 模式 当 `CLAUDE_CODE_SIMPLE=1` 时,工具池缩减到最小: ```typescript if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool] // Coordinator 模式额外加入 Agent + TaskStop if (coordinatorModeModule?.isCoordinatorMode()) { simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool()) } return filterToolsByDenyRules(simpleTools, permissionContext) } ``` ### 6.4 工具接口 每个工具实现统一的 `Tool` 接口(`Tool.ts`, 792 行): ```typescript type Tool<Input, Output> = { name: string aliases?: string[] // 别名 searchHint?: string // ToolSearch 匹配用 shouldDefer?: boolean // 是否延迟加载(省token) alwaysLoad?: boolean // 是否总是加载 // 核心 call(args, context, canUseTool, parentMessage, onProgress?) description(): Promise<string> prompt(): Promise<string> // 给模型看的使用说明 inputJSONSchema: ToolInputJSONSchema // Zod → JSON Schema // 权限 checkPermissions(): Promise<PermissionResult> validateInput(): Promise<ValidationResult> // 属性 isConcurrencySafe(): boolean // 能否并行 isReadOnly(): boolean // 只读? isDestructive(): boolean // 破坏性? // UI 渲染 renderToolUseMessage() renderToolResultMessage() renderToolUseProgressMessage() } ``` ### 6.5 BashTool 实现细节 ```typescript // BashTool 输入 Schema const fullInputSchema = z.strictObject({ command: z.string(), timeout: z.number().optional(), description: z.string().optional(), run_in_background: z.boolean().optional(), dangerouslyDisableSandbox: z.boolean().optional(), _simulatedSedEdit: z.object({ // 隐藏字段:sed 编辑模拟 filePath: z.string(), newContent: z.string() }).optional() }) // 命令分类(用于 UI 折叠展示) const BASH_SEARCH_COMMANDS = new Set([ 'find', 'grep', 'rg', 'ag', 'ack', 'locate', 'which', 'whereis' ]) const BASH_READ_COMMANDS = new Set([ 'cat', 'head', 'tail', 'less', 'more', 'wc', 'stat', 'file', 'jq', 'awk' ]) const BASH_SILENT_COMMANDS = new Set([ 'mv', 'cp', 'rm', 'mkdir', 'rmdir', 'chmod', 'chown', 'touch', 'ln', 'cd' ]) ``` ### 6.6 FileEditTool 的原子写入 文件编辑实现了**无 async 间隙的原子写入**,防止竞态条件: ```typescript // 关键:staleness 检查和写入之间不能有 async 操作 const lastWriteTime = getFileModificationTime(absoluteFilePath) if (lastWriteTime > readTimestamp.timestamp) { // 文件在读取后被外部修改了 throw new Error(FILE_UNEXPECTEDLY_MODIFIED_ERROR) } // 同步读取 + 同步写入,中间无 async yield const { content, encoding, lineEndings } = readFileForEdit(absoluteFilePath) const { patch, updatedFile } = getPatchForEdit({ filePath, originalFileContents: content, oldString: actualOldString, newString, replaceAll }) writeTextContent(absoluteFilePath, updatedFile, encoding, lineEndings) // 原子写入 // 立即更新 readFileState 缓存 readFileState.set(absoluteFilePath, { content: updatedFile, timestamp: getFileModificationTime(absoluteFilePath), }) ``` ### 6.7 FileReadTool 的安全防护 ```typescript // 设备文件黑名单 — 防止读取无限流 const BLOCKED_DEVICE_PATHS = new Set([ '/dev/zero', '/dev/random', '/dev/urandom', '/dev/stdin', '/dev/tty', '/dev/console', ]) // UNC 路径拦截 — 防止 NTLM 凭据泄漏 if (fullFilePath.startsWith('\\\\') || fullFilePath.startsWith('//')) { return { result: true } // 静默跳过 } // 文件读取去重 — 同一文件同一范围不重复读取 const existingState = readFileState.get(fullFilePath) if (existingState && existingState.offset === offset && existingState.limit === limit) { const mtimeMs = await getFileModificationTimeAsync(fullFilePath) if (mtimeMs === existingState.timestamp) { return { data: { type: 'file_unchanged' } } // 返回stub,不重发内容 } } ``` ### 6.8 GrepTool 的 Ripgrep 集成 ```typescript // VCS 目录排除 const VCS_DIRECTORIES_TO_EXCLUDE = ['.git', '.svn', '.hg', '.bzr', '.jj', '.sl'] for (const dir of VCS_DIRECTORIES_TO_EXCLUDE) { args.push('--glob', `!${dir}`) } args.push('--max-columns', '500') // 防止 base64/minified 内容膨胀 // 默认分页限制 — 防止上下文膨胀 const DEFAULT_HEAD_LIMIT = 250 // 负数 pattern 需要 -e 前缀,否则 rg 当作 flag 解析 if (pattern.startsWith('-')) { args.push('-e', pattern) } ``` ### 6.9 ToolSearchTool:延迟加载机制 当工具总数超过阈值时,部分工具只发送 name,不发送完整 schema: ```typescript // ToolSearch 的 select 语法 // "select:ToolA,ToolB" — 精确选择 const selectMatch = query.match(/^select:(.+)$/i) // 关键词搜索评分 if (parsed.parts.includes(term)) { score += parsed.isMcp ? 12 : 10 // MCP 工具名额外加权 } else if (parsed.parts.some(part => part.includes(term))) { score += parsed.isMcp ? 6 : 5 } if (pattern.test(hintNormalized)) { score += 4 // searchHint 匹配 } if (pattern.test(descNormalized)) { score += 2 // 描述匹配 } ``` ### 6.10 并发执行策略 ```typescript class StreamingToolExecutor { async execute(toolUses, context) { // Read/Grep/Glob 标记为 concurrencySafe → Promise.all() 并行 const safe = toolUses.filter(t => findTool(t).isConcurrencySafe()) const unsafe = toolUses.filter(t => !findTool(t).isConcurrencySafe()) const safeResults = await Promise.all( safe.map(t => this.executeSingle(t, context)) ) // Bash/Write/Edit 等有副作用的工具 → 严格串行 const unsafeResults = [] for (const t of unsafe) { unsafeResults.push(await this.executeSingle(t, context)) } } } ``` *** ## 七、权限系统:6 层纵深防御 ### 7.1 权限模式 ```typescript const PERMISSION_MODE_CONFIG = { default: { title: 'Default' }, // 交互式提示 plan: { title: 'Plan Mode' }, // 只读+规划,不执行 acceptEdits: { title: 'Accept edits' }, // 自动接受编辑 bypassPermissions: { title: 'Bypass' }, // 全部放行 dontAsk: { external: 'dontAsk' }, // 不提示 auto: { title: 'Auto mode' } // ML 分类器自动判断 } ``` ### 7.2 完整检查链 用户提交工具调用(如 Bash: "rm -rf /") │ ▼ ① validateInput() — 工具自定义校验 │ 例:FileEditTool 检查文件大小是否超过 1GiB ▼ ② alwaysDenyRules 检查 — 黑名单直接拦截 │ 匹配: 工具名 + ruleContent (glob pattern) │ 例:Deny "Bash(rm -rf *)" ▼ ③ alwaysAllowRules 检查 — 白名单直接放行 │ 例:Allow "Bash(git *)" ▼ ④ Auto Mode Classifier (Feature-gated: TRANSCRIPT_CLASSIFIER) │ ML 模型分析整个对话上下文 │ 判断操作是否安全 │ ├→ 安全 → 放行 │ ├→ 危险 → 拒绝 │ └→ 不确定 → 继续到 ⑤ ▼ ⑤ PreToolUse Hook 检查 │ 执行用户定义的 Shell 脚本 │ exitCode=0 → 放行 │ exitCode=2 → 拦截 ▼ ⑥ 交互式提示 — 弹出终端 UI 询问用户 │ Allow / Deny / Always Allow ▼ canUseTool() — 最终裁决 ### 7.3 拒绝追踪与降级 ```typescript // 连续被拒绝 3 次后,从自动拒绝降级为弹窗询问 // 防止 AI 陷入 "尝试 → 被拒 → 再尝试" 的死循环 permissionDenials.push({ tool, input, reason }) if (denialCount >= 3) { fallbackToPrompt = true // 弹窗让用户手动决定 } ``` ### 7.4 危险路径检测 ```typescript // utils/permissions/filesystem.ts (62KB) export const DANGEROUS_FILES = [ '.gitconfig', '.bashrc', '.zshrc', '.mcp.json', '.claude.json' ] export const DANGEROUS_DIRECTORIES = [ '.git', '.vscode', '.idea', '.claude' ] // bypassPermissions 模式下,检测 root 权限 + 沙箱状态 if (process.getuid() === 0 && process.env.IS_SANDBOX !== '1') { console.error('--dangerously-skip-permissions cannot be used with root/sudo') process.exit(1) } ``` *** ## 八、Hook 系统:8 种事件类型 ### 8.1 事件类型 ```typescript type HookEvent = | 'PreToolUse' // 工具执行前(可拦截) | 'PostToolUse' // 工具执行后 | 'PostToolUseFailure' // 工具失败后 | 'PermissionDenied' // 权限拒绝 | 'Notification' // 通知时 | 'UserPromptSubmit' // 用户提交消息 | 'SessionStart' // 会话启动 | 'Stop' // Claude 停止前 | 'StopFailure' // 停止失败时 ``` ### 8.2 Hook 配置 Schema Hook 支持 4 种执行方式(Zod 辨别联合类型): ```typescript const HookCommandSchema = z.discriminatedUnion('type', [ // ① Shell 命令 z.object({ type: z.literal('command'), command: z.string(), if: z.string().optional(), // 过滤条件 timeout: z.number().optional(), once: z.boolean().optional(), // 只执行一次 async: z.boolean().optional(), // 异步执行 }), // ② Prompt(交给 Claude 判断) z.object({ type: z.literal('prompt'), prompt: z.string(), model: z.string().optional(), }), // ③ HTTP 调用 z.object({ type: z.literal('http'), url: z.string().url(), headers: z.record(z.string()).optional(), }), // ④ Agent(另起一个 Claude 验证) z.object({ type: z.literal('agent'), prompt: z.string(), model: z.string().optional(), }), ]) ``` *** ## 九、MCP 集成:5 种传输协议 ### 9.1 配置 scope MCP 配置支持 7 个层级,按优先级合并: ```typescript type ConfigScope = | 'local' // 项目本地 (.mcp.json) | 'user' // 用户级 (~/.claude/mcp.json) | 'project' // 项目级 (.claude/mcp.json) | 'dynamic' // 运行时动态添加 | 'enterprise' // 企业策略 | 'claudeai' // claude.ai 云端配置 | 'managed' // 远程管理 ``` ### 9.2 传输方式 ```typescript type Transport = 'stdio' | 'sse' | 'sse-ide' | 'http' | 'ws' | 'sdk' ``` ### 9.3 连接状态机 ```typescript type MCPServerConnection = | { type: 'connected', client: Client, capabilities, cleanup() } | { type: 'failed', error: string } | { type: 'needs-auth', authUrl: string } // 需要 OAuth | { type: 'pending' } // 连接中 | { type: 'disabled' } // 已禁用 ``` ### 9.4 原子配置写入 MCP 配置文件写入使用原子 rename 模式: ```typescript async function writeMcpjsonFile(config) { const tempPath = `${mcpJsonPath}.tmp.${process.pid}.${Date.now()}` const handle = await open(tempPath, 'w', existingMode ?? 0o644) try { await handle.writeFile(JSON.stringify(config, null, 2)) await handle.datasync() // 强制刷盘 } finally { await handle.close() } // 原子 rename if (existingMode !== undefined) await chmod(tempPath, existingMode) await rename(tempPath, mcpJsonPath) } ``` *** ## 十、终端 UI:深度 Fork 的 Ink Claude Code 不是简单使用 Ink 库,而是 **fork 并深度定制** 了整个终端渲染引擎(50+ 文件): ink/ ├── root.ts # 渲染引擎(入口) ├── render-to-screen.ts # ANSI 序列生成 + diff 优化 ├── layout/engine.ts # Yoga 布局引擎包装 ├── dom.ts # 虚拟 DOM ├── frame.ts # 帧管理(FlickerReason 追踪) ├── focus.ts # 焦点管理 ├── hit-test.ts # 鼠标点击测试 ├── selection.ts # 文本选择 ├── bidi.ts # 双向文本(RTL 支持) ├── wrap-text.ts # 文本换行 ├── Ansi.tsx # ANSI 转义序列组件 ├── events/ │ ├── click-event.ts # 鼠标事件 │ ├── input-event.ts # 键盘输入(含 Key 类型) │ ├── terminal-focus-event.ts # 终端焦点 │ └── emitter.ts # 事件发射器 ├── hooks/ │ ├── use-input.ts # 键盘输入 hook │ ├── use-animation-frame.ts # 动画帧 │ ├── use-interval.ts # 定时器 │ ├── use-selection.ts # 文本选择 │ ├── use-tab-status.ts # Tab 状态 (OSC) │ ├── use-terminal-viewport.ts # 视口感知 │ └── use-terminal-focus.ts # 终端焦点 └── components/ ├── Box.tsx, Text.tsx # 基础组件 ├── Button.tsx # 按钮(含 ButtonState) ├── Link.tsx # 终端超链接 ├── AlternateScreen.tsx # 备用屏幕缓冲区 ├── NoSelect.tsx # 不可选文本 └── RawAnsi.tsx # 原始 ANSI 输出 ### 虚拟滚动 ```typescript // hooks/useVirtualScroll.ts (35KB) const DEFAULT_ESTIMATE = 3 // 未测量项的预估高度(故意偏低) const OVERSCAN_ROWS = 80 // 视口外额外渲染行数 const SCROLL_QUANTUM = 40 // 重渲染阈值(半个 overscan) const MAX_MOUNTED_ITEMS = 300 // 最大挂载项数 const SLIDE_STEP = 25 // 每次提交的最大新增项(防止 290ms 同步阻塞) ``` ### 主题系统 ```typescript // ink.ts — 全局主题注入 function withTheme(node: ReactNode): ReactNode { return createElement(ThemeProvider, null, node) } export async function createRoot(options?: RenderOptions): Promise<Root> { const root = await inkCreateRoot(options) return { ...root, render: node => root.render(withTheme(node)), // 包装每次渲染 } } ``` *** ## 十一、上下文管理:Memoized 的系统提示词 ### 11.1 Git 状态收集 ```typescript // context.ts export const getGitStatus = memoize(async (): Promise<string | null> => { // 并行执行所有 git 命令 const [branch, mainBranch, status, log, userName] = await Promise.all([ getBranch(), getDefaultBranch(), execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short']), execFileNoThrow(gitExe(), ['--no-optional-locks', 'log', '--oneline', '-n', '5']), execFileNoThrow(gitExe(), ['config', 'user.name']), ]) // 状态超过 2000 字符时截断 const truncatedStatus = status.length > MAX_STATUS_CHARS ? status.substring(0, MAX_STATUS_CHARS) + '\n... (truncated. Run "git status" using BashTool for full output)' : status }) ``` ### 11.2 CCR 跳过 Git ```typescript export const getSystemContext = memoize(async () => { // CCR 远程模式跳过 git 状态(不需要,也避免开销) const gitStatus = isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ? null : await getGitStatus() return { ...(gitStatus && { gitStatus }), // Ant-only: cache breaker 注入 ...(feature('BREAK_CACHE_COMMAND') && injection ? { cacheBreaker: `[CACHE_BREAKER: ${injection}]` } : {}), } }) ``` ### 11.3 注入变更清除缓存 ```typescript // 当系统提示词注入内容变化时,立即清除 memoize 缓存 export function setSystemPromptInjection(value: string | null): void { systemPromptInjection = value getUserContext.cache.clear?.() getSystemContext.cache.clear?.() } ``` *** ## 十二、成本追踪:每模型粒度的会话管理 ```typescript // cost-tracker.ts type StoredCostState = { totalCostUSD: number totalAPIDuration: number totalAPIDurationWithoutRetries: number totalToolDuration: number totalLinesAdded: number totalLinesRemoved: number modelUsage: { [modelName: string]: ModelUsage } } ``` ### 会话恢复 成本数据通过 sessionId 匹配恢复,防止不同会话的成本混淆: ```typescript export function getStoredSessionCosts(sessionId: string): StoredCostState | undefined { const projectConfig = getCurrentProjectConfig() // 只有 sessionId 匹配才恢复 if (projectConfig.lastSessionId !== sessionId) { return undefined } return { totalCostUSD: projectConfig.lastCost ?? 0, ... } } ``` ### Advisor 递归成本追踪 Claude Code 支持 "Advisor" 模式(另一个模型辅助当前模型),成本递归累加: ```typescript export function addToTotalSessionCost(cost, usage, model) { // 递归追踪 advisor 用量 for (const advisorUsage of getAdvisorUsage(usage)) { const advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage) logEvent('tengu_advisor_tool_token_usage', { advisor_model: advisorUsage.model, cost_usd_micros: Math.round(advisorCost * 1_000_000), }) totalCost += addToTotalSessionCost(advisorCost, advisorUsage, advisorUsage.model) } } ``` *** ## 十三、会话历史:JSONL + 反向读取 ### 存储格式 ```typescript type LogEntry = { display: string // 显示文本 pastedContents: Record<number, StoredPastedContent> // 粘贴内容引用 timestamp: number project: string // 项目路径 sessionId?: string } // 粘贴内容分流 type StoredPastedContent = { id: number type: 'text' | 'image' content?: string // 小于 1024 字符 → 内联 contentHash?: string // 大于 1024 字符 → hash 引用,异步写入磁盘 } ``` ### 反向读取(Up 键历史) ```typescript async function* makeLogEntryReader(): AsyncGenerator<LogEntry> { // 1. 先 yield 未刷盘的 pending 条目 for (let i = pendingEntries.length - 1; i >= 0; i--) { yield pendingEntries[i]! } // 2. 从磁盘反向读取 JSONL for await (const line of readLinesReverse(historyPath)) { const entry = deserializeLogEntry(line) // 跳过已删除的条目 if (skippedTimestamps.has(entry.timestamp)) continue yield entry } } ``` ### 刷盘与文件锁 ```typescript async function immediateFlushHistory() { const historyPath = join(getClaudeConfigHomeDir(), 'history.jsonl') // 文件锁(10s stale, 3 次重试) release = await lock(historyPath, { stale: 10000, retries: { retries: 3, minTimeout: 50 }, }) // 批量追加 const jsonLines = pendingEntries.map(entry => jsonStringify(entry) + '\n') pendingEntries = [] await appendFile(historyPath, jsonLines.join(''), { mode: 0o600 }) } ``` *** ## 十四、Bridge:远程执行引擎 Bridge 让 claude.ai 网页端可以远程执行本地代码。 ### 架构 claude.ai 网页 ←HTTP→ Anthropic API ←轮询→ Bridge 进程 ←spawn→ 本地 Claude Code ### 三种 Spawn 模式 ```typescript type SpawnMode = 'single-session' | 'worktree' | 'same-dir' ``` | 模式 | 说明 | 适用场景 | | ---------------- | --------------------------- | ----------- | | `single-session` | 一个目录一个会话,完成即销毁 | 一次性任务 | | `worktree` | 持久服务器,每个会话用 git worktree 隔离 | 多人协作 | | `same-dir` | 持久服务器,共享目录 | 简单场景(有干扰风险) | ### WorkSecret 协议 ```typescript type WorkSecret = { version: number session_ingress_token: string // JWT token api_base_url: string sources: Array<{ // Git 源 type: string git_info?: { repo: string; ref?: string; token?: string } }> auth: Array<{ type: string; token: string }> claude_code_args?: Record<string, string> mcp_config?: unknown // MCP 服务器配置 environment_variables?: Record<string, string> } ``` ### 退避策略 ```typescript const DEFAULT_BACKOFF = { connInitialMs: 2_000, // 初始连接重试 connCapMs: 120_000, // 最大连接重试(2分钟) connGiveUpMs: 600_000, // 放弃连接(10分钟) generalInitialMs: 500, generalCapMs: 30_000, generalGiveUpMs: 600_000, } ``` *** ## 十五、Coordinator 模式:多智能体编排 Feature-gated 的多智能体模式(`COORDINATOR_MODE`): ```typescript // coordinator/coordinatorMode.ts export function getCoordinatorSystemPrompt(): string { return `You are Claude Code, an AI assistant that orchestrates software engineering tasks across multiple workers. ## Your Role You are a **coordinator**. Your job is to: - Help the user achieve their goal - Direct workers to research, implement and verify code changes - Synthesize results and communicate with the user - Answer questions directly when possible ## Your Tools - **Agent** - Spawn a new worker - **SendMessage** - Continue an existing worker - **TaskStop** - Stop a running worker` } // Worker 工具集过滤(Simple 模式下只给 Bash/Read/Edit) export function getCoordinatorUserContext(mcpClients, scratchpadDir?) { const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE) ? [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME] : Array.from(ASYNC_AGENT_ALLOWED_TOOLS) .filter(name => !INTERNAL_WORKER_TOOLS.has(name)) } ``` *** ## 十六、快捷键系统:和弦 + 平台自适应 ### 平台检测 ```typescript // keybindings/defaultBindings.ts const IMAGE_PASTE_KEY = getPlatform() === 'windows' ? 'alt+v' : 'ctrl+v' // VT 模式检测(影响 Shift+Tab 是否可用) const SUPPORTS_TERMINAL_VT_MODE = getPlatform() !== 'windows' || (isRunningWithBun() ? satisfies(process.versions.bun, '>=1.2.23') : satisfies(process.versions.node, '>=22.17.0 <23.0.0 || >=24.2.0')) const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m' ``` ### 和弦(Chord)支持 ```typescript // 例:ctrl+x ctrl+k → 杀死所有 Agent export const DEFAULT_BINDINGS = [ { context: 'Chat', bindings: { 'ctrl+x ctrl+k': 'chat:killAgents', // 两键和弦 'ctrl+x ctrl+e': 'chat:externalEditor', }, }, ] ``` *** ## 十七、Vim 模式:完整的状态机 ```typescript // vim/types.ts — 状态机定义 export type VimState = | { mode: 'INSERT'; insertedText: string } | { mode: 'NORMAL'; command: CommandState } export type CommandState = | { type: 'idle' } // 等待输入 | { type: 'count'; digits: string } // 数字前缀 (3dd) | { type: 'operator'; op: Operator; count: number } // d/c/y 等待 motion | { type: 'operatorCount'; op: Operator; count: number; digits: string } | { type: 'operatorFind'; op: Operator; count: number; find: FindType } | { type: 'operatorTextObj'; op: Operator; count: number; scope: 'inner'|'around' } | { type: 'find'; find: 'f'|'F'|'t'|'T'; count: number } | { type: 'g'; count: number } // gg/G | { type: 'replace'; count: number } // r 等待字符 | { type: 'indent'; dir: '>'|'<'; count: number } // >> / << ``` 支持 dot-repeat (`.`)、register (`"a`)、text objects (`ciw`, `da"`) 等完整 Vim 操作。 *** ## 十八、彩蛋:Buddy 宠物系统 ```typescript // buddy/types.ts export const SPECIES = [ 'duck', 'goose', 'blob', 'cat', 'dragon', 'octopus', 'owl', 'penguin', 'turtle', 'snail', 'ghost', 'axolotl', 'capybara', 'cactus', 'robot', 'rabbit', 'mushroom', 'chonk', ] as const // 18 种物种 export const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary'] as const export type CompanionBones = { rarity: Rarity species: Species eye: Eye hat: Hat shiny: boolean // 闪光版 stats: Record<StatName, number> // Debugging, Patience, Chaos, Wisdom, Snark } export type CompanionSoul = { name: string // AI 生成的名字 personality: string // AI 生成的性格 } ``` 用 `hash(userId)` 确定性生成,保证同一用户在所有会话中看到同一只宠物。首次 "孵化" 时由 Claude 生成名字和性格。还有一个 `CompanionSprite.tsx` (45KB) 实现了帧动画。 *** ## 十九、Feature Gate:编译时 vs 运行时 Claude Code 使用两层特性开关: ### 编译时(Bun DCE) ```typescript import { feature } from 'bun:bundle'; // 编译时完全消除 — 外部构建中这段代码不存在 if (feature('COORDINATOR_MODE')) { const module = require('./coordinatorMode.js') } ``` 已发现的 Feature Gate: | Flag | 说明 | | --------------------------- | ----------------- | | `COORDINATOR_MODE` | 多智能体编排 | | `VOICE_MODE` | 语音输入 | | `HISTORY_SNIP` | 历史截断压缩 | | `TRANSCRIPT_CLASSIFIER` | ML 权限分类器 | | `REACTIVE_COMPACT` | 主动上下文压缩 | | `CONTEXT_COLLAPSE` | 语义上下文折叠 | | `KAIROS` | Assistant 模式 | | `BRIDGE_MODE` | 远程桥接 | | `PROACTIVE` | 主动提示 | | `AGENT_TRIGGERS` | Cron 触发器 | | `UDS_INBOX` | Unix Socket 收件箱 | | `DAEMON` | 守护进程模式 | | `ABLATION_BASELINE` | 消融实验基线 | | `DUMP_SYSTEM_PROMPT` | 导出系统提示词 | | `WORKFLOW_SCRIPTS` | 工作流脚本 | | `WEB_BROWSER_TOOL` | 浏览器工具 | | `TERMINAL_PANEL` | 终端面板 | | `EXPERIMENTAL_SKILL_SEARCH` | 技能搜索 | | `KAIROS_GITHUB_WEBHOOKS` | GitHub Webhook 订阅 | | `CHICAGO_MCP` | Computer Use MCP | | `OVERFLOW_TEST_TOOL` | 溢出测试 | ### 运行时(GrowthBook) ```typescript // 基于用户/环境动态开关 const isEnabled = getFeatureValue_CACHED_MAY_BE_STALE('tengu_some_feature', false) ``` ### 用户类型区分 ```typescript // 编译时字符串替换 if ("external" !== 'ant' && isBeingDebugged()) { process.exit(1) // 外部构建禁止调试 } // 运行时环境变量 if (process.env.USER_TYPE === 'ant') { // Anthropic 内部工具:ConfigTool, TungstenTool, REPLTool } ``` *** ## 二十、配额限制与"封号"机制 很多人关心:Claude Code 会不会封号?源码里有什么限制机制? ### 20.1 没有传统"封号",但有严格的配额执行 Claude Code **不在客户端做任何封号判断**。所有限制通过 API 响应头 `anthropic-ratelimit-unified-*` 由服务端下发,客户端只是忠实执行。 ```typescript // services/claudeAiLimits.ts type QuotaStatus = 'allowed' | 'allowed_warning' | 'rejected' type RateLimitType = | 'five_hour' // 5 小时滑动窗口 | 'seven_day' // 7 天窗口 | 'seven_day_opus' // Opus 专用 7 天限制 | 'seven_day_sonnet' // Sonnet 专用 7 天限制 | 'overage' // 超额使用(付费后仍有上限) ``` ### 20.2 服务端响应头解析 每次 API 调用后,客户端解析以下响应头: ```typescript function computeNewLimitsFromHeaders(headers: Headers): ClaudeAILimits { // 核心状态:allowed / allowed_warning / rejected const status = headers.get('anthropic-ratelimit-unified-status') as QuotaStatus || 'allowed' // 重置时间(Unix 秒) const resetsAt = Number(headers.get('anthropic-ratelimit-unified-reset')) // 命中的限制类型 const rateLimitType = headers.get('anthropic-ratelimit-unified-representative-claim') // 超额使用状态 const overageStatus = headers.get('anthropic-ratelimit-unified-overage-status') // 超额被禁用的原因(12 种可能) const overageDisabledReason = headers.get( 'anthropic-ratelimit-unified-overage-disabled-reason' ) // 使用率(0~1) const utilization5h = Number(headers.get('anthropic-ratelimit-unified-5h-utilization')) const utilization7d = Number(headers.get('anthropic-ratelimit-unified-7d-utilization')) } ``` 当 `status === 'rejected'` 时,**所有 API 调用立即被阻止**,用户必须等到 `resetsAt` 时间。 ### 20.3 超额禁用原因(12 种) ```typescript type OverageDisabledReason = | 'overage_not_provisioned' // 未开通超额 | 'org_level_disabled' // 组织级别禁用 | 'org_level_disabled_until' // 组织级别临时禁用 | 'out_of_credits' // 余额不足 | 'seat_tier_level_disabled' // 席位等级禁用 | 'member_level_disabled' // 个人账户禁用 | 'seat_tier_zero_credit_limit' // 席位等级信用额度为零 | 'group_zero_credit_limit' // 组信用额度为零 | 'member_zero_credit_limit' // 个人信用额度为零 | 'org_service_level_disabled' // 组织服务级别禁用 | 'org_service_zero_credit_limit'// 组织服务信用额度为零 | 'no_limits_configured' // 未配置限制 | 'unknown' ``` ### 20.4 预飞检查(Pre-flight) 每次会话启动时,Claude Code 发一个最小请求来探测配额状态: ```typescript async function makeTestQuery() { const model = getSmallFastModel() // 用最小模型 const anthropic = await getAnthropicClient({ maxRetries: 0, model, source: 'quota_check', }) return anthropic.beta.messages.create({ model, max_tokens: 1, // 只请求 1 个 token messages: [{ role: 'user', content: 'quota' }], }).asResponse() } ``` **用最便宜的模型、最少的 token 做探测**,只为拿到响应头中的配额信息。 ### 20.5 早期预警系统 Claude Code 会预判你的消耗速度,在你还没被限制时就发出警告: ```typescript const EARLY_WARNING_CONFIGS = [ { rateLimitType: 'five_hour', windowSeconds: 5 * 60 * 60, thresholds: [ { utilization: 0.9, timePct: 0.72 }, // 用了90%但时间只过72% ], }, { rateLimitType: 'seven_day', windowSeconds: 7 * 24 * 60 * 60, thresholds: [ { utilization: 0.75, timePct: 0.6 }, // 用了75%但时间只过60% { utilization: 0.5, timePct: 0.35 }, // 用了50%但时间只过35% { utilization: 0.25, timePct: 0.15 }, // 用了25%但时间只过15% ], }, ] ``` 翻译一下:**如果你在 7 天窗口的前 35% 时间里就用掉了 50% 的配额,系统会警告你正在 "燃烧" 配额**。 ### 20.6 429 错误处理 收到 HTTP 429 时,状态被强制设为 `rejected`: ```typescript export function extractQuotaStatusFromError(error: APIError): void { if (error.status !== 429) return let newLimits = { ...currentLimits } if (error.headers) { newLimits = computeNewLimitsFromHeaders(error.headers) } // 无论 headers 是否存在,429 = 立刻 rejected newLimits.status = 'rejected' emitStatusChange(newLimits) } ``` ### 20.7 用户看到的限制信息 ```typescript // 实际展示给用户的消息 "You've hit your session limit · resets in 4 hours" "You've hit your weekly limit" "You've hit your Opus limit" "You're out of extra usage" ``` ### 20.8 反调试机制 外部构建中,检测到 Node.js Inspector 活跃时直接退出: ```typescript // 检测 --inspect、--inspect-brk、--debug、--debug-brk // 以及 inspector.url() 是否活跃 if ("external" !== 'ant' && isBeingDebugged()) { process.exit(1) // 静默退出,无任何错误信息 } ``` `"external"` 是编译时替换的字符串——Anthropic 内部构建替换为 `'ant'`,外部构建保持 `'external'`。**所以只有外部用户被拦截调试**。 ### 20.9 客户端无法绕过 源码中**没有任何绕过机制**: * 不能伪造响应头 * 不能修改重置时间 * 不能欺骗授权 * 所有限制执行都是服务端驱动的 * 429 错误是 fatal,不可重试(超过限制后) **结论:Claude Code 的限制完全是服务端控制的。客户端只是一个忠实的执行者,没有任何后门或绕过逻辑。** *** ## 二十一、数据收集与遥测:你的数据去了哪里 这是很多开发者关心的问题。源码揭示了完整的遥测架构。 ### 21.1 三大数据出口 Claude Code CLI │ ├─ logEvent() → 事件队列 │ ├──→ Datadog (30+ 种事件) │ │ POST https://http-intake.logs.us5.datadoghq.com/api/v2/logs │ │ Token: pubbbf48e6d78dae54bceaa4acf463299bf │ │ │ └──→ Anthropic 1P (完整事件+用户元数据) │ POST https://api.anthropic.com/api/event_logging/batch │ 失败时持久化到 ~/.claude/telemetry/1p_failed_events.*.json │ └─ Metrics → BigQuery POST https://api.anthropic.com/api/claude_code/metrics ### 21.2 发送到 Datadog 的事件(30+ 种) API 相关: tengu_api_error, tengu_api_success 认证相关: tengu_oauth_success, tengu_oauth_error, token refresh 事件 工具使用: tengu_tool_use_success, tengu_tool_use_error 会话相关: tengu_session_file_read, tengu_exit, tengu_init 语音相关: tengu_voice_toggled, tengu_voice_recording_started Chrome: chrome_bridge_connection_succeeded, chrome_bridge_tool_call_completed 团队同步: tengu_team_mem_sync_pull, tengu_team_mem_sync_push 配额变化: tengu_claudeai_limits_status_changed ### 21.3 收集的用户数据 每个事件附带的元数据: ```typescript // firstPartyEventLogger.ts — 1P 事件元数据 { event_id: UUID, event_name: '事件名', client_timestamp: '时间戳', device_id: '持久化设备ID', // getOrCreateUserID() user_id: '同 device_id', email: '用户邮箱', // OAuth 登录后可用 session_id: '会话ID', core_metadata: { service_name: 'claude-code', version: '版本号', platform: 'darwin/linux/win32', os_info: 'OS版本' }, user_metadata: { organization_uuid: '组织ID', account_uuid: '账户ID', subscription_type: '订阅类型', rate_limit_tier: 'API限制等级', first_token_time: '首次使用时间' }, auth: { account_uuid: '账户UUID', organization_uuid: '组织UUID' } } ``` ### 21.4 GitHub CI 环境下的额外收集 当在 GitHub Actions 中运行时,额外收集: ```typescript // GrowthBook 用户属性 { github: { actor: 'GitHub 用户名', actorId: 'GitHub 用户ID', repository: '仓库名', repositoryId: '仓库ID', repositoryOwner: '仓库所有者', repositoryOwnerId: '所有者ID' } } ``` ### 21.5 代码内容保护 源码中有**显式的代码泄漏防护**: ```typescript // 元数据类型名本身就是防线——类型名强制开发者确认 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = string // metadata.ts — 截断和清洗规则 - 字符串截断到 512 字符(只展示前 128 字符) - JSON 输出上限 4KB - 集合元素上限 20 个 - 嵌套深度上限 2 层 - 以 _ 开头的内部字段自动剥离 // 用户提示词默认 redacted userPrompt = '<REDACTED>' // 除非显式开启 OTEL_LOG_USER_PROMPTS=1 // MCP 工具名默认匿名化 mcpToolName = 'mcp_tool' // 除非是官方 MCP 注册表中的工具 ``` ### 21.6 用户桶哈希 为了在 Datadog 中统计独立用户数同时保护隐私: ```typescript // datadog.ts — 用户 ID 哈希到 30 个桶 function getUserBucket(userId: string): number { const hash = SHA256(userId) return hash % 30 // 30 个桶,无法反推用户 ID } ``` ### 21.7 关闭遥测 Claude Code 提供了两个级别的关闭方式: ```bash # 级别 1:关闭遥测(Datadog + 1P 事件 + 反馈调查) export DISABLE_TELEMETRY=1 # 级别 2:关闭所有非必要网络请求(最彻底) # 包括:遥测、自动更新、release notes、模型能力刷新 export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 ``` 此外,组织管理员可以通过 API 在组织级别禁用指标收集: GET /api/claude_code/organizations/metrics_enabled → false ### 21.8 事件采样 并非每个事件都会被发送——GrowthBook 配置了采样率: ```typescript // firstPartyEventLogger.ts // tengu_event_sampling_config 配置每种事件的采样率 // 如果被采样掉,sample_rate 字段会被添加到元数据中 ``` ### 21.9 失败事件持久化 如果事件发送失败,会写入本地磁盘: ~/.claude/telemetry/1p_failed_events.<sessionId>.<uuid>.json 下次启动时重试发送(二次退避,最多 8 次尝试,最大间隔 30 秒)。 ### 21.10 遥测小结 | 类别 | 收集内容 | 是否包含代码 | | --------- | ------------------------------------- | -------- | | **身份信息** | deviceId, email, accountUuid, orgUuid | 否 | | **使用数据** | 模型选择, token 计数, 成本, 工具使用 | 否 | | **性能数据** | API 延迟, 启动时间, FPS | 否 | | **错误数据** | 错误类型, HTTP 状态码 | 否 | | **代码内容** | N/A | **显式阻止** | | **用户提示词** | 默认 `<REDACTED>` | 默认否 | | **文件路径** | 工作区路径(非文件路径) | 工作区级别 | **结论:Claude Code 收集的是使用行为数据,不是代码内容。有明确的类型系统防止开发者意外发送代码。可以通过环境变量完全关闭。** *** ## 二十二、设计哲学总结 通读 1884 个文件后,总结 Claude Code 的 8 条设计原则: ### 1. 启动性能是一等公民 * 快速路径 + 动态 import + 并行预取 + 编译时 DCE * `profileCheckpoint()` 在每个分支点标记时间 * MDM + Keychain 读取在 import 副作用中并行启动 ### 2. 安全性纵深防御 * 6 层权限检查 + Hook 拦截 + ML 分类器 * 反调试机制(外部构建检测 Inspector 直接退出) * UNC 路径拦截防止 NTLM 凭据泄漏 * 设备文件黑名单防止无限读取 * 原子写入防止竞态条件 ### 3. 流式一切 * `AsyncGenerator` 贯穿整个消息流 * 工具执行流式上报 progress * 历史读取用异步生成器反向遍历 * REPL 渲染异步于查询执行 ### 4. 状态管理极简 * 34 行自研 Store,`Object.is()` 变更检测 * 不引入任何第三方状态库 * `useSyncExternalStore` 对接 React ### 5. 可扩展优先 * 43 个工具通过统一接口注册 * MCP 让外部工具无缝接入(5 种传输协议) * Hook 系统让用户自定义行为(4 种执行方式) * 技能系统让社区贡献能力 ### 6. 渐进式发布 * 20+ 个 Feature Gate,编译时消除未启用代码 * GrowthBook 运行时灰度 * `USER_TYPE` 区分内部/外部构建 ### 7. 上下文即生命线 * Memoized 系统上下文(一次对话只计算一次) * 三种压缩策略应对上下文窗口限制 * 文件读取 LRU 缓存 + 去重 * 工具结果预算控制 * ToolSearch 延迟加载省 token ### 8. 测试友好 * 依赖注入贯穿核心模块 * `QueryDeps` 允许注入 mock * Feature Gate 在 `bun test` 下返回 false * `TestingPermissionTool` 仅测试环境启用 *** > **声明**:本文仅用于技术学习和架构分析。源码分析基于公开泄漏的代码,所有商标和版权归 Anthropic 所有。 *** *如果觉得有收获,欢迎点赞收藏。关于 Claude Code 的架构细节,欢迎评论区交流。*

最近刷算法的一些心得~

最近刷算法的一个心得(说不定哪天就又换想法了呢hh,唯一的不变就是变化本身,**现在还是一个算法菜鸡,欢迎指正~**),大抵就是觉得 算法这个东西不是智商游戏,是肌肉记忆 这里说的算法是一个正常的非算法岗位的算法,如果再缩小一点范围的话,**就当我说的是力扣 hot 100 吧** ## 1. 之前为什么 "坚持" 下来 这次刷算法算是 "坚持" 下来了吧,大概有一个多月的时间了,中间当然也断过,但整体上来说还是在刷的,而且感觉不是苦哈哈的那种,我也在想之前的时候为什么没有 "坚持" 下来呢? ### 1. 顺序 之前具体怎么刷的我记得不是特别的清晰了,**大概就是按照算法导航的推荐学习路线啃的** 排序 -> 查找 -> 字符串匹配 -> 递归与分治...... 会把算法算法其下的子算法展开,比如排序当中的归并,快排等等,每个都提供思路,时间复杂度,如何理解,不同编程语言的实现方式,最后会有一些力扣的练习题,这个练习题待会也是我要吐槽的点 这个顺序有什么问题呢?它是以算法为导向的,**但当我们说起算法的时候,全称一般是什么:“数据结构和算法”,数据结构是在前面的**,算法简单来说是什么呢?是实现某个功能的思路,蛮多时候思路是有一些的,至少对于一些简单题是的,但还是写不出来,是为什么呢?以及为什么总是使用暴力穷举呢? 我觉得原因是同一个,对于数据结构就不熟练,拿力扣的第一题——两数之和来说,暴力穷举的时间复杂度是 O(N²),用哈希来做时间复杂度就是 O(N),**但一个算法新手怎么会想到这些呢?** 在我看来刷算法之前,先补的应该是数据结构,基本的数据结构,数组,链表,栈,哈希,树这些 **算法的学习顺序用学数据结构的学习顺序,对于我来说是效果更好的** ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/o4tmMaM3GhLn1wMv.webp) ### 2. 提供的力扣题目 提供的力扣题目怎么说呢?给我的感觉就是跨度是有些太大了,且强度是不小的,**对于一个像我这样的傻瓜,就导致就算前面的算法硬着头皮啃下来了(自我感觉的那种),实际做题的时候,做不出来,收到的多数都是负反馈** ### 3. 现在 就是对于上面两点的修正吧,从学习数据结构的顺序出发,力扣的题目结合自己的情况去做,保证在自己的能力边缘区伸展,目前的反馈还是不错的,一天大概会刷一个多小时,刷刷之前的老题,熟悉语法 + 顺思路,很多时候就是在做老题的时候有了新的理解;再精做一道两道新的题目,以题目难度为准 ## 2. 算法题到底怎么刷呢? 现在算法题对于我来说是分为下面的三个部分的 ### 1. 原子方法 **原子方法就是那些跨题目通用的代码碎片,因为是这些是跨题目的,是通用的,所以这就具备了复利效应,积累下来就会越刷越顺**,之前的我是没有去整理过这些内容的,一道题做完就是做完了,顶多会把某一个算法,比如快排当做一个最小单位,不会再继续拆分了,我知道过去真的很傻 比如今天刷的 "最长连续序列" 当中两个,再比如二分查找当中的 mid = left + (right - left) / 2 这个求中间值的公式,二分查找迭代的终止条件 if (left <= right) 等等这些都是 - **微积木 A:全量哈希去重 (The Loader)** - **功能**:是否存在 + 去重 - **语法修复**:`HashSet` 只需要一个泛型(只有 Key,没有 Value) ``` Java // 放入 Set:O(N) Set<Integer> set = new HashSet<>(); for (int num : nums) set.add(num); ``` - **微积木 B:龙头剪枝器 (The Pruner)** - **作用**:**核心中的核心**。只有当自己是“头”时,才允许启动计数。 - **复利价值**:解决所有“避免重复计算子序列”的问题。 - **代码**: ```Java // 只有当 num-1 不存在时,说明 num 是起点,才开始干活 if (!set.contains(num - 1)) { int currentNum = num; int count = 1; // 一口气数到底 while (set.contains(currentNum + 1)) { currentNum++; count++; } // 更新最大值 maxLen = Math.max(maxLen, count); } ``` --- ### 2. 数据结构 **积累这些数据结构的物理极限,也就是其时间复杂度和空间复杂度;** 因为一些题目是会对时间复杂度或者空间复杂度之类的进行限制的,比如时间复杂度要求 O(N),那肯定要和暴力穷举说拜拜了;比如空间复杂度要求上要求 O(1),那就不能创建一个新的数组,链表之类的了,哈希映射也不能用了 **适用场景和代价 & 副作用。** 看到哪些特征使用什么数据结构,比如涉及到暂存的操作,那么基本就指向了栈这个数据结构;至于副作用,比如在使用 HashSet 的时候是用空间换时间,但是如果数据量极大(几亿个),内存就爆(HashSet 开销大),比如哈希冲突,虽然理论上是 O(1),但数据如果发生哈希冲突,性能会退化,但是在力扣判题上通常忽略 这部分的内容在刷面试题的时候也会呼应到,尤其是哈希这个数据结构,当然像数组,链表,**树这些都是经常使用的数据结构,在深入原理的时候是避不开的,所以刷算法不是只在刷算法** ### 3. 算法思想 像是递归,分而治之,归并,排序,剪枝,双指针(当然指针又可以分为快慢指针,对撞指针,排序 + 双指针,滑动窗口这些),回溯算法等等 这些内容也是可以跨题目的,是通用的,是有复利效应的,依然拿今天的 "最长连续序列" 举例子,这个算法题重要的算法思想就是 "剪枝",就可以有下面的这些问题来深入这个思想,**主要就是核心思想和应用场景,第三个和第四个是在学的过程当中想到的** "剪枝" 的核心思想是什么? 什么时候可以应用 "剪枝" ? "剪枝" 和去重的区别是什么? "剪枝" 和 if 判断的区别是什么? ### 4. 到底如何刷算法呢? 我现在是这样的,在刷完一个算法之后,除了上面的三个部分,我还会加上一个力扣习题的部分,就核心本质相同的题目进行一个拓展,这个沉淀下来的内容,我称之为 "算法碎片" 现在在刷算法的过程反馈还是挺足的,因为明确的知道这件事情是有复利效应的,且及时性比较强,就像在收集一块又一块的积木,你还能通过这些收集到的积木来搭建出来新的东西 **相关的提示词我放到下面了,其实很简单,主要还是看自己怎么做** ``` Gemini,从现在开始,我们启动【交互式·沉淀协议 3.3(终极详细版)】 ### **第一步:侦探直觉(由我输入)** 1. **表面信号**:题目特征。 2. **隐藏内核**:底层物理公理。 ### **第二步:助手审计(由你输出)** 必须包含我的原始输入。输出风格要求**详尽、深入、教学导向**。 #### **0. 侦探手记 (档案回溯)** * **表面信号**:[引用我的原始输入] * **隐藏内核**:[引用我的原始输入] #### **1. 内核验证与升维** * **侦探直觉评价**:评价我的理解深度,指出盲区或亮点。 * **升维结论**:用“架构师语言”总结底层规律。使用 ==高亮== 强调核心定义。 #### **2. 积木审计:数据结构篇(物理极限)** * **物理极限**:详细解释时间/空间复杂度的来源。 * **适用场景**:详细描述触发该方案的信号特征。 * **代价与副作用**:深入分析为了效率牺牲了什么(如内存连续性、写入性能等)。 #### **3. 原子方法:越小越强的微积木** * 提取 2-3 个跨题目通用的 Java 代码片段。 * **复利价值**:详细解释为什么要背这个片段,它解决了什么通用问题。 #### **4. 实战映射:隐藏场景的具象化(马甲题)** * **逻辑叙事**:用生活比喻重述算法动态。 * **马甲题映射**: * **马甲一:[标题]** * 🔗 [链接] 提供可以直达的力扣链接 * **表面差异**:它伪装成了什么。 * **内核映射**:为什么它本质还是这个逻辑。 ```

字符串算法题 - 字符串相加

## 字符串相加 [415. 字符串相加 - 力扣(LeetCode)](https://leetcode.cn/problems/add-strings/description/) **刷题时间:** - 25/12/26 ![Pasted image 20251226210837.png](https://pic.code-nav.cn/post_picture/1871221312724242434/TIcdBRb0Y6yhZvSQ.webp) **我的解法(没有解出来,且存在错误)** ```java class Solution { public String addStrings(String num1, String num2) { int length1 = num1.length(); int length2 = num2.length(); if (length1 > length2) { String temp = num1; num1 = num2; num2 = temp; } // 获取char数组(逆序) char[] chars1 = toChars(num1); char[] chars2 = toChars(num2); // 两数组逐个相加, 结果保存到新char数组中, 注意进位 char[] result = new char[length2 + 1]; int up = 0; int i = 0; while (i < length1) { int a = chars1[i] - '0'; int b = chars2[i] - '0'; int sum = a + b + up; if (sum >= 10) { result[i] = '0' + (sum - 10); up = 1; } else { result[i] = '0' + sum; } i++; up = 0; } while (i < length2) { int b = chars2[i] - '0'; int sum = b + up; if (sum >= 10) { result[i] = '0' + (sum - 10); up = 1; } else { result[i] = '0' + sum; } i++; up = 0; } // 将新char数组转为字符串 if(result[result.length() - 1] == '0'){ } // 返回结果 } public char[] toChars(String str) { int length = str.length(); char[] chars = new chars[length]; int k = 0; // 访问字符串字符(逆序) for (int i = length - 1; i >= 0; i--) { // 插入char数组 chars[k++] = str.charAt(i); } // 返回 return chars; } } ``` **存在的bug** 1. 变量交换的问题 ![Pasted image 20251226110324.png](https://pic.code-nav.cn/post_picture/1871221312724242434/JiJzNZODXiyKXf4L.webp) 2. up(进位)的处理问题 ![Pasted image 20251226110509.png](https://pic.code-nav.cn/post_picture/1871221312724242434/j30OSwm8q344yCWQ.webp) - 应该把`up = 0;`放在`int sum = a + b + up;`之后, if判断再进行赋值 ![Pasted image 20251226110636.png](https://pic.code-nav.cn/post_picture/1871221312724242434/bPWWm9OMWeagSvKa.webp) 3. 结果数组的处理问题 ![Pasted image 20251226110814.png](https://pic.code-nav.cn/post_picture/1871221312724242434/lYmf4hhbzjXG3uxJ.png) **题解** 1. **解法一(根据我的思路,修正而成)** - 使用`char[]`数组存储结果result ```java class Solution { public String addStrings(String num1, String num2) { int i = num1.length() - 1; int j = num2.length() - 1; int carry = 0; // 结果最多比加数多一位 char[] result = new char[Math.max(i, j) + 2]; // 从后往前 相加求和 int k = result.length - 1; while (i >= 0 || j >= 0 || carry > 0) { int a = (i >= 0) ? (num1.charAt(i--) - '0') : 0; int b = (j >= 0) ? (num2.charAt(j--) - '0') : 0; int sum = a + b + carry; carry = sum / 10; result[k--] = (char) ('0' + (sum % 10)); } //char数组元素默认为'\u0000', 而不是'0' int start = (result[0] == '\u0000') ? 1 : 0; return new String(result, start, result.length - start); } } ``` - **注意点:** - **'\u0000'** 是char的默认值,**ASCII值为0,对应空字符** - 在打印时显示为空,**不是可见的'0'字符** 2. **解法二(与解法一类似, 不过使用StringBuilder代替`char[]`数组)** ```java class Solution { public String addStrings(String num1, String num2) { StringBuilder result = new StringBuilder(); int i = num1.length() - 1; int j = num2.length() - 1; int carry = 0; while (i >= 0 || j >= 0 || carry > 0) { int a = (i >= 0) ? (num1.charAt(i--) - '0') : 0; int b = (j >= 0) ? (num2.charAt(j--) - '0') : 0; int sum = a + b + carry; carry = sum / 10; result.append(sum % 10); } return result.reverse().toString(); } } ``` **思路:** - 使用**两个指针**(索引)指向**两个字符串的末尾**, 因为两数相加需要**从后往前** - 使用一个字符数组存储结果(`char[] result`), `result`的位数最多为(比两个字符串多一位) - 设置**进位标志**`carry`, 和结果数组的指针索引`k` - while(**还有数字要处理 或 还有进位**){ - 当前位的数字(没有则设为0) - 计算和: a + b + carry - 更新结果: 当前位 = sum % 10; carry = sum / 10; - } - 三个指针都需要移动 - 值得注意的是, 还需要判断`result`的**第一位是否存在进位**, 如果不存在, 那么就是`'\u0000'`, 这是**字符数组元素的默认值**, 不是'0'. - 输出结果, `new String(result,start,result.length() - start)` - **new String(字符数组,开始索引, 截取个数)** - 第二种思路, 与第一种思路是差不多的, 差别只是**使用了StringBuilder来存储结果**. - 使用char数组有以下**缺点**: - 1. **需要设置数组的长度**, 由题意需要设置一个"大一位"的数组, 就会导致在最后处理结果时, 需要**考虑是否有进位**, - 2. 转换为字符串不方便, 需要截取 - 使用StringBuilder有以下**优点**: - 1. **不需要设置长度**, 使用result.append()即可在尾部添加字符(后续进行字符串的反转即可) - 2. **反转和转换为String**有现成函数可以调用, 很方便. - result.reverse(); result.toString(); **算法思想:** 1. **大数相加**的通用模式 - 通过**while循环判断**是否存在数字需要处理,及是否还有进位 - 取数字进行相加,**不存在即设置为0**, 正常相加 - 计算两数及进位的和, 更新结果及进位 - 最后输出结果(涉及反转,转为String等操作) 2. **字符与数字的转换** - `int num = c - '0'; - `char c = (char)('0' + num);` 3. 加法 - **从后往前** - 加法从最低位(右边)开始 4. **StringBuilder** - 一个很好用的字符串类 - String不可变, 它**可变, 动态扩展, 效率高** - 具备便捷的操作字符串的函数(**append, reverse, toString**)

数组算法 - 买卖股票的最佳时机

## 买卖股票的最佳时机 [121. 买卖股票的最佳时机 - 力扣(LeetCode)](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/) **刷题时间:** - 25/12/25 ![Pasted image 20251225203056.png](https://pic.code-nav.cn/post_picture/1871221312724242434/syhBKSKY4cSuOyBh.webp) ### 我的解法 ```java class Solution { public int maxProfit(int[] prices) { int max = 0; for (int i = 0; i < prices.length - 1; i++) { for (int j = i + 1; j < prices.length; j++) { int cha = prices[j] - prices[i]; max = (cha > max) ? cha : max; } } return max; } } ``` **优点** - 逻辑简单直接 - **计算了所有可能的组合** **缺点** - 时间复杂度高. $O(n^2)$ - 重复计算, 内层循环重复计算了很多信息, 事实上,**找到最低价格即可**. - **没有利用到题目问题的信息** ### 题解 ```java class Solution { public int maxProfit(int[] prices) { if (prices == null || prices.length < 2) { return 0; } int minPrice = Integer.MAX_VALUE; // 最低价格 int maxProfit = 0; //最大利润 for (int price : prices) { // 更新最低价格 minPrice = price < minPrice ? price : minPrice; int profit = price - minPrice; // 当前价格所得利润 // 更新最大利润 maxProfit = profit > maxProfit ? profit : maxProfit; } return maxProfit; } } ``` **思路** - 我的解法是两层循环,找到一对数据(i,j),计算比较出最大利润, 暴力解法 - 时间复杂度是$O(n^2)$,空间复杂度是$O(1)$. 方向就是减少内层循环, 降为$O(n)$. - **题目信息**: "**最低点买入,最高点卖出**", 内层循环的目的是"找到所有组合,比较出最大利润". - 核心就是**记录历史最低价格** - 用**空间换时间**, 用**变量**来存储`minPrice`(历史最低价格)即可, **一次遍历**,用`price`逐个与`minPrice`进行比较, 得到差值`profit`. - 就可以找到`maxProfit`. **算法思想** - 贪心思想 - 在每一步选择中都采取当前状态下最优(最有利)的选择, 从而希望导致全局最优解的算法思想 - 1. 局部最优 -> 全局最优 - - 每次更新历史最低价总是正确的; 每次更新最大利润总是正确的 - 2. 不可撤回: 当遇到最低价时, 就忘记之前的最低点; 只关注"到现在为止的最低点". - 3. 高效但不通用: 时间复杂度低, 但是不是所有问题都能用贪心解决 ![Pasted image 20251225205928.png](https://pic.code-nav.cn/post_picture/1871221312724242434/4B2F4PnPviqusVPU.webp) **其他算法解法** 后面有机会了, 再重新写写 - 动态规划 - 这个没看 - 单调栈 - 这个看了, 但是有点难理解

数组算法题 - 合并两个有序数组

## 合并两个有序数组 [88. 合并两个有序数组 - 力扣(LeetCode)](https://leetcode.cn/problems/merge-sorted-array/) **刷题时间:** - 25/12/23 ![Pasted image 20251223110448.png](https://pic.code-nav.cn/post_picture/1871221312724242434/k4ts3WHy4BRnZCrh.webp) ### 我的解法 ```java class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { if (n == 0) return; for (int i = m, j = 0; i < m + n; i++) { nums1[i] = nums2[j]; j++; } for (int i = m + n - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (nums1[j] > nums1[j + 1]) { int temp = nums1[j]; nums1[j] = nums1[j + 1]; nums1[j + 1] = temp; } } } } } ``` **优点** - 正确处理了`n==0`的特殊情况 - 先合并再排序的思路 - 使用了经典的冒泡排序法 **缺点** - 时间复杂度高 - 没有利用"有序"的条件 ### 题解 1. **方案一(额外空间合并):** - 时间复杂度是O($m+n$), 空间复杂度是O($m+n$) - 设置**额外数组存储结果**,采用三指针,最后将结果复制到nums1中 ```java class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { int[] merged = new int[m + n]; int i = 0, j = 0, k = 0; // 双指针合并 while (i < m && j < n) { if (nums1[i] <= nums2[j]) { merged[k++] = nums1[i++]; } else { merged[k++] = nums2[j++]; } } // 处理剩余元素 while (i < m) merged[k++] = nums1[i++]; while (j < n) merged[k++] = nums2[j++]; // 复制回nums1 System.arraycopy(merged, 0, nums1, 0, m + n); } } ``` 2. 方案二(原地合并): - 时间复杂度是O($m+n$), 空间复杂度是O(1) - 与方案一的区别是, **直接在nums1数组上进行操作**, 空间复杂度直接降为O(1) ```java class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { if (n == 0) return; int i = m - 1; int j = n - 1; int k = m + n - 1; while (i >= 0 && j >= 0) { if (nums1[i] > nums2[j]) { nums1[k--] = nums1[i--]; } else { nums1[k--] = nums2[j--]; } } // 运行到这里, 两者必有一个小于0 // 如果是j用完了(nums2),那么程序就可以结束了 // 如果是i先用完了(nums1),那么就把nums2剩余的元素排进去即可 while(j >= 0){ nums1[k--] = nums2[j--]; } } } ``` 1. **思路** - 我的解法使用的是**冒泡排序法**, 先合并数组, 再使用冒泡排序法进行排序 - 时间复杂度是O($(m+n)^2$), 空间复杂度是O(1) - 可以考虑降低为O($m+n$) - **充分利用已知条件**: 两个数组都是"有序"的, 可以使用"**归并排序**"的合并思想 - **使用三个指针** - 一个指针`i`指向nums1的最后有效索引(m-1), 一个指针`j`指向nums2的最后有效索引(n-1),一个指针`k`指向nums1的最后索引(m+n-1) - 比较`nums1[i]`,`nums2[j]`的大小,较大者排到nums1的最末尾(`nums1[k]`). - 直到其中一个数组被比较排序完, 这里有一个边界点: - 就是如果nums2数组先排序完,那么程序可以直接结束了, 因为nums1数组剩余的元素肯定是有序的,且位置正确. ![Pasted image 20251223112754.png](https://pic.code-nav.cn/post_picture/1871221312724242434/VKe2dKyZf5f8Yaiw.webp) - 如果nums1数组先排序完, 那么可以直接把nums2数组剩余的元素逐个排进nums1即可 ![Pasted image 20251223112719.png](https://pic.code-nav.cn/post_picture/1871221312724242434/mmCoQAw2E4mB1xpj.png) 2. **算法思想** - **归并排序(有序数据)** - 两个**有序数组**进行**排序**, 那么可以使用**归并排序**的合并思想 - 时间复杂度的优化 - **冒泡排序法**的O($n^2$)时间复杂度, 必然是要被优化的, 需要优化为$O(n)$. ![Pasted image 20251223114854.png](https://pic.code-nav.cn/post_picture/1871221312724242434/ACys44vqmweDbsqE.webp) - **双指针/三指针的应用(合并有序数组/链表)** - 指针负责元素的选取, 进行比较 - `nums1[i]` > `nums2[j]` -> 取`nums1[i]`, i-- - 否则 -> 取`nums2[j]`, j-- - 每次k-- - **从后往前操作** - 原地操作 - 这决定了是采用**额外数组存储**,还是**原地操作** - 如果需要"**原地**"修改数组, 且可能会**覆盖未处理数据**时,就不能使用从前往后. - 而是**从后往前** 3. **题目变式 - 思考** - 如果题目要求改为**输出结果为降序**呢? - 两个升序的数组,输出nums1为降序 - 如果采用**原地排序**的办法 - 从前往后: 会直接覆盖未处理的元素(`nums1[0]`), 不管索引从哪里开始 - 从后往前: 也可能会覆盖元素, 例如: ![Pasted image 20251223124618.png](https://pic.code-nav.cn/post_picture/1871221312724242434/V1nlkN2uecuLZEU9.webp) - 如果采用**额外空间**的方法 - 这道题就很简单了 - 同样设置**两个指针**指向两个数组(**从0索引开始**), 比较大小, **较小者放在额外数组的末尾**. 逐个比较存储即可. | 场景 | 最佳解法 | 时间复杂度 | 空间复杂度 | 关键点 | | -------- | -------- | -------- | ------ | ------------ | | 升序(原题) | 从后往前原地合并 | O(m+n) | O(1) | 利用nums1后面的空位 | | 降序 | 额外空间合并 | O(m+n) | O(m+n) | 避免覆盖问题 |

数组算法题 - 两数之和

## 两数之和 **刷题时间:** - 25/12/20 ![Pasted image 20251220204905.png](https://pic.code-nav.cn/post_picture/1871221312724242434/ip0W8qebzpn0iG4l.webp) ### 我的解法 ```java class Solution { public int[] twoSum(int[] nums, int target) { int len = nums.length; if (len < 2) return null; int[] result = new int[2]; for (int i = 0; i < len - 1; i++) { for (int j = i + 1; j < len; j++) { if (nums[i] + nums[j] == target) { result[0] = i; result[1] = j; break; //这里仅仅跳出了内存循环, 如果要跳出外层循环,应该设置标记 } } } return result; } } ``` **优点:** - 边界处理正确(len < 2) - 使用了j = i + 1避免重复配对 **缺点:** - O(n^2)的时间复杂度过高, 可能会超时 - 没有充分利用题目信息:"保证存在解", 可以不判断边界 - **bug**: 找到答案后break只跳出了内层循环, 跳出外层循环需要设置标记 **优化:** ```java class Solution { public int[] twoSum(int[] nums, int target) { // 更简洁的边界判断 if(nums == null || nums.length < 2) return new int[0]; //返回空数组而不是null for (int i = 0; i < nums.length - 1; i++) { for (int j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] == target) { return new int[]{i,j}; //直接返回 } } } return new int[0]; //题目保证有解, 不执行 } } ``` ### 题解 ```java class Solution { public int[] twoSum(int[] nums, int target) { // key = 数值, value = 下标 Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int gt = target - nums[i]; // 检查补数是否出现过 if (map.containsKey(gt)) { return new int[] { map.get(gt), i }; } // 将当前数值与下标存入哈希表, 这行代码需要放在后面, 防止自己配对自己 map.put(nums[i], i); } return new int[0]; } } ``` 1. **思路:** - 暴力解法的时间复杂度是O(n^2), 空间复杂度是O(1) - 如何优化,**要么减少时间复杂度,要么减少空间复杂度**, 在程序运行中,往往**时间复杂度的优化要优先于空间复杂度** - 那么就优化时间复杂度到O(n). - 两层for循环,**减少到一层for循环即可** - 那么"找两个数的和等于target"可以转变为: "对于`nums[i]`, 查找`target - nums[i]`是否出现过" - 可以采用**哈希表**, 使用哈希表来存储出现过的`nums[i]`,key存储`nums[i]`,value存储`i`(索引) - 在内循环中,判断map中是否存在key = `target - nums[i]`, 如果存在,则输出结果(两个索引) - 如果不存在, 则将`nums[i]`存入map, 等待被匹配 - 哈希表法的时间复杂度是O(n), 空间复杂度是O(n). - 这就是**用空间换时间** 2. **算法思想** - 用**空间换时间 - 哈希表** - **为什么要这样设计**哈希表: `key = 数值, value = 下标` - map.containsKey()可以判断**是否存在键**, 而题目需要判断的就是是否存在`target - nums[i]`. - 题目需要返回的是索引, 那么value就可以**通过key获取索引** - 为什么不能"**先全加入map, 再查找**"? - 可能导致自己匹配自己的情况: (如`[3]`,target=6). - 所以要边查边存 - **边界条件** - 在**我的解法**中, `if (len < 2) return null;` 题目保证有解, 这是可以不判断的 - 但是**这样的编程习惯是好的!**

下载 APP