I’m Christophe

Welcome to my blog

My LLM codegen workflow atm

My LLM codegen workflow atm, Harper Reed在文中介绍了基于LLM的代码生成工作流。主要介绍了两种场景,开发一个新项目(Greenfield)以及老项目的持续迭代(Non-greenfield)。 新项目基于需求细化(spec),计划制定(todo)以及代码生成三部分来开展。老项目则通过生成代码上下文(repomix)来制定测试回归和代码审查任务。这对我来说是一个巨大的启发,准备找时间试一下其中描述的工作流程。 同时Harper Reed也给出了具体的Prompt,局限于目前LLM的特性,仍然需要通过提示工程来引导AI生成我们需要的内容。前一阵子较火的DeepSeek从入门到精通也指出掌握提示语设计是AIGC时代的必备技能,在平时使用这些LLM工具中也感受到怎么清晰的向AI表达需求非常重要,因为你的提示语完全决定了AI生成的质量,进而决定了AI是否好用或者为你带来提效。另外,由于中文的特性(高上下文依赖)以及模型的训练数据分布,可能会出现提示效果不如英文的情况。

Go 1.24

Go 1.24 Release Notes, Go 1.24已经于2.11正式发布,主要语言特性为泛型类型别名和基于Swiss Tables的map实现,通过轻微的delete性能损耗来显著提升查询和插入的性能(via)。

Three Observations

Three Observations, 奥尔特曼提出了关于AI经济学的3个论断: AI模型的智能水平大致等于用于训练和运行它的资源的对数; 使用特定水平的AI的成本每12个月大约下降10倍(摩尔定律18个月才有2倍); 线性增加的智能所带来的社会经济价值是超指数级的。 随着DeepSeek-R1的火爆,整个社会更广泛的进入了AI时代,一众大厂应用纷纷接入,普通人使用的成本进一步下降。之前一直有观点认为AI会逐步淘汰部分岗位,并且这种事情会首先发生在程序员这个职业上。目前就我个人的体验来讲,AI现在已经能做到生成不错的代码,处理较为复杂的任务。虽然在整个公司层面还没有开始大规模在开发流程中使用AI,但我认为这是未来近几年的方向,届时会出现Cursor、通义灵码这些产品的终极形态,AI变成你的同事。而作为程序员的我们,工作方式会更向需求分析、架构设计和团队协作等需要人类创造力和判断力的方向靠拢(via)。

Restart As a Link Blog

Build a link blog, 最近从Simon Willison看到的想法,觉得是一个重新捡起Blog的机会。这个依托于Github Pages 的博客站点荒废了好久,最早可以追溯到2018年,彼时还未大学毕业,中间有一段时间荒废,后续迁移到博客园上重启,过来一段时间后又荒废。如今又重新捡起,说实话整体感受还是比较复杂的,之前荒废的原因无外乎以下几点: 懒; 觉得无法输出有意思的观点,只是在拾人牙慧; 由于2,导致写blog过程中没收到什么反馈,没有反馈的事情,对人类来说还是太难坚持了。 但是近期从Simon Willison那了解到了关于blog内容的新思路: It’s easy to get hung up on this. I’ve definitely felt the self-imposed pressure to only write something if it’s new, and unique, and feels like it’s never been said before. This is a mental trap that does nothing but hold you back. 这个我深有同感,一开始其实整体内容围绕着一些常见的学习知识点、读书内容,慢慢就发现,如果说我不能输出独特的观点,这值得我写一篇文章/博客吗?进而转到如果我的博客没人看,我还要花时间在上面吗?甚至之前还觉得我应该写英文文章,这样部署在Github Pages才有更多的读者。说回来,Simon Willison认为blog的价值在于保持长期的写作习惯,同时随着时间的推移有所收获, 同时Simon Willison给出了写blog的几个内容(via): TIL(Things I Learned); DMP(Descriptions of My Projects); TIF(Things I’ve Found)。 基于3就衍生出了本文的主题,“links”, 也就是说通过blog的形式分享记录自己阅读的内容。作为一个Link Blog, 可以包含以下内容: ...

函数式数据结构漫谈(一)

近期计划开这个系列的坑,内容大多都是“Purely Functional Data Structures”内容加一点自己的理解(改一张牌就是我的了:),算是打磨文笔? 数据结构是什么 当我们在讨论数据结构的时候,我们在讨论什么。常见的介绍有“数据结构是一种数据组织、管理和存储的格式,它可以帮助我们实现对数据高效的访问和修改,更准确地说, 数据结构是数据值的集合,可以体现数据值之间的关系,以及可以对数据进行应用的函数或操作”。然而到更具体的场景,数据结构的概念还能够细化,比如我们经常会 讨论函数栈怎么怎么样,这里的“函数栈”也是数据结构,但它是一个泛指,是一个在程序执行过程中存在的概念,或者叫标识。Okasaki在他的“Purely Functional Data Structures”里指出,数据结构这一概念通常有四种含义: 抽象,即抽象数据类型(abstract data type,可以用Java中的interface理解),即表示数据的类型和一组适用于该类型的函数; 实现,即对应于ADT的一个具体实现,通常是指对于该ADT做的具体设计; 实例,即在程序运行中对应于一个数据类型的具体实例; 泛指,即在程序运行中一个泛指的概念,不涉及具体的实例,例如上文提到的函数栈。 本系列将使用Haskell作为描述语言,则其中class可对应抽象的概念,data可对应实现的概念。具体到Java,可以用interface对应抽象的概念,用class对应实现 的概念。 函数式强调的是什么 抛开函数式编程本身强调的函数以外(不然就没法讲了),函数式编程通常还强调不可变(immutable)。因此,当我们说一个数据结构符合函数式的特性的时候主要在讨论不可变, 或者说持久性(persistence)。换句话说,在更新一个函数式的数据结构之后,它更新前的版本我们仍然能够访问到。这意味着所有有着破坏式更新的数据结构都不符合这一性质, 同时也表明相较于能够进行破坏式更新的数据结构,函数式数据结构的性能可能会更差,通常会有一个对数阶的更新代价在里面。实现持久性的方式非常简单,只需要将原有的数据结构 复制一遍,然后在复制后的数据结构上更新,由于没有破坏式的更,可以通过共享不变的部分来减少开销。下面讨论在函数式编程中经典的list。 List list在任何编程语言中都是非常常见的存在,函数式编程对其讨论则更多,著名的Lisp就取自“LISt Processor”。我们首先来看广泛的list定义 1 2 3 4 5 6 7 class List t where empty :: t a isEmpty :: t a -> Bool cons :: a -> t a -> t a -- error if the list is empty. head :: t a -> a tail :: t a -> t a 这里可以考虑将head和tail的结果包装一个Maybe,使之适合空的list,在这种情况下isEmpty就不再需要,因为head ls = Nothing或 tail ls = Nothing已经暗含isEmpty ls = true。本文为了偷懒就没用这种定义)。有了这些,我们就可以实现list上的各种“更高级的” 操作,例如经典的map: 1 2 3 4 map :: List t => (a -> b) -> t a -> t b map f ls = if isEmpty ls then emptll else cons (f $ head ls) (map f $ tail ls) 从这个定义上可以直接看出,这个List是不支持随机访问的。因此,我们额外定义支持“按下标”随机访问的class: ...

Memoization in Haskell

Memoization是动态规划(Dynamic Programming)中自顶向下处理问题采用的策略, 其基本想法是通过将子问题的解保存起来避免重复计算来优化算法. 这个概念本身很简单, 在其他有明显mutable语义的语言中, 实现起来也非常简单. 但是在Haskell中问题就变的复杂了不少, 对于一个原始的函数f :: a -> b你如果要用ref, 比如说IORef, 你必须要把它放到IO monad中, 你的memoize函数就变成了... -> IO (a -> b). 我们希望是能够找到一个memoize :: ... -> (a -> b), 这样memoize之后得到的和原函数类型是一致的. 为了讨论的方便, 我们主要关注两个例子的memoization, 一个是经典的Fibonacci数列: 1 2 3 4 fib :: Int -> Integer fib 0 = 0 fib 1 = 1 fib n = fib (n - 2) + fib (n - 1) 另一个则是动态规划(自底向上)中典型的最小编辑距离的问题, 所谓的最小编辑距离就是一个字符串通过增加, 删除, 替换的操作得到另一个字符串所需要的操作次数: 1 2 3 4 5 6 minEditDist :: String -> String -> Int minEditDist [] [] = 0 minEditDist s [] = length s minEditDist [] s = length s minEditDist (x:xs) (y:ys) | x == y = minEditDist xs ys | otherwise = 1 + minimum [minEditDist xs ys, minEditDist xs (y:ys), minEditDist (x:xs) ys] Memoizing with specific problem 首先来看fib的问题, wiki给出了一个非常elegant的解(就fib本身而言, 还有更经典的解, fib = (fibs !!) where fibs = 0 : 1 : zipWith (+) fibs (tail fibs)): ...

Surpasser Count

Pearl 2: 给定一个长度大于1的列表, 计算其元素的最大surpasser count, 要求算法复杂度 $O(n log n)$. Type: msc: Ord a => [a] -> Int “Pearls of functional algorithm design"的第二章, 我们先来看surpasser的定义 Definition surpasser: 称列表中$X[j]$是$X[i]$的surpasser, 如果$X[i] < X[j]$且$i < j$. 因此一个元素的surpasser count就是其surpasser的数目. 同样, 一个naive的实现很容易: 1 2 3 4 msc :: Ord a => [a] -> Int msc xs = maximum [scount z zs | z:zs <- tails xs] scount :: Ord a => a -> [a] -> Int scount x xs = length $ filter (> x) xs 同时也很容易看到, 这个实现的时间复杂度是 $O(n^2)$, 不符合要求的 $O(n log n)$. 为了达到 $O(n log n)$ 的时间复杂度, 我们希望有个函数f能够递归的处理xs = us ++ vs, 并且存在一个线性复杂度的函数join, 使得f xs = join (f us) (f vs), 这样整体的复杂度满足 $T(n)=2 T(n/2)+O(n)=O(n log n)$. 原文中, 作者利用分治的思想通过一步步地推导获得了线性时间的join, 这里也仅仅是类似于复读的"再解释”. ...

The Smallest Free Number

Pearl 1: 给定一个自然数的有限集X, 计算不属于X的最小自然数. X表示为不包含重复元素的无序列表. 时间复杂度要求$O(n)$. Type: minfree :: [Int] -> Int(也可以额外的定义自然数类型, 不过这不是我们的重点) “Pearls of Functional Algorithm Design"的第一章, 其描述了一个分治的算法和一个基于array的算法, 这里按个人的思路讲解一下基于分治的算法, 基于array的算法具体可以查阅原文. 首先拿到这个问题, 我觉得最直接的想法就是 Base Solution: minfree xs = head $ [0..] \\ xs 然而这和要求的线性时间复杂度不符. 第二个想法就是设计一个fold的函数遍历一遍列表, 这样时间复杂度符合要求. 但是越来越多的边界条件让我意识到思路不对. 看了原文才发现忽略了解题的一个重要条件. Fact: [0..n]中的所有自然数不可能都在X(xs)中, 其中n = length xs. 这也很容易证明, 因为$ n + 1 = length\ [0..n] > n $, 因此不属于集合X的最小自然数就是[0..n]中不属于X的最小自然数. 至此,该问题很容易解决, 只需要一个marked的array来表示[0,,n]中的自然数是否在X中即可. 下面描述基于分治的算法, 首先给出一个基本的结论. Theorem: (as ++ bs) \\ (us ++ vs) == (as \\ us) ++ (bs \\ vs), 如果as \\ vs == as && bs \\ us == bs. ...