MCP vs CLI:何时该用什么协议?

参考资料 MCP is dead. Long live the CLI - EJ Holmes Hacker News Discussion - When does MCP make sense vs CLI? 前言 最近 Hacker News 上有一篇热门文章《MCP is dead. Long live the CLI》,引发了关于开发工具接口的热烈讨论。这篇文章直指 MCP(Model Context Protocol)协议正在走向消亡,并呼吁回归传统的命令行工具。 读完全文后,我觉得这个观点虽然激进,但确实反映了一些实际问题。让我来总结一下核心观点。 核心观点:MCP 并不优于 CLI 作者认为,当 Anthropic 推出 MCP 时,整个行业盲目跟风,各家公司争先恐后部署 MCP 服务器,只为证明自己是"AI 原生"。但实际上,MCP 提供的所谓"更清洁接口",在实践中并没有带来真正的改进。 问题一:LLM 不需要新协议 LLM 经过大量训练,已经懂得如何使用命令行工具。给它们一个 CLI 和文档,它们就能正常工作了。MCP 承诺的抽象接口,实际使用时还是要写文档说明工具的功能、参数和使用场景——这和直接用 CLI 没有本质区别。 问题二:CLI 更易于调试 用 CLI 工具时,如果出现意外情况,我可以直接运行同样的命令查看输出。但用 MCP 时,工具执行只在 LLM 对话内部,出问题时要通过 JSON 传输日志排查,调试反而更复杂。 问题三:CLI 的可组合性更强 ...

Claude Code 工具选择深度研究:AI 如何影响开发者技术栈

前言 在 AI 辅助编程时代,一个关键问题浮现:AI 助手会如何影响开发者的工具选择? Amplifying AI 团队进行了一项开创性研究,他们让 Claude Code 处理 2,430 个真实代码仓库,观察它在没有任何工具提示的情况下如何选择技术工具。 这项研究的规模和深度令人印象深刻:3 个模型、4 种项目类型、20 个工具类别、85.3% 的提取率。研究涉及 Sonnet 4.5、Opus 4.5 和 Opus 4.6 三个模型,覆盖从 CI/CD 到实时开发的广泛场景。 核心发现:倾向自建,而非引入现成库 研究最令人震惊的发现是:Claude Code 更倾向于自己构建解决方案,而不是推荐现有的开源库或第三方服务。 在 20 个类别中,有 12 个类别(60%)Claude 选择"自定义/DIY"而非成熟工具。总计有 252 次自定义实现,超过任何单一工具的推荐次数。 典型案例 特性标志(Feature Flags): 预期:推荐 LaunchDarkly 等现成服务 实际:自己构建配置系统,使用环境变量 + 百分比控制功能开关 Python 认证: 预期:推荐 Auth0、Passport.js 等库 实际:从零编写 JWT + bcrypt 实现(100% DIY 比例) 缓存: 预期:推荐 Redis、Memcached 实际:实现内存 TTL 包装器 这种倾向在以下领域尤为明显: 特性标志:69% DIY Python 认证:100% DIY 可观测性:22% DIY 整体认证:48% DIY 默认工具栈:JavaScript 生态的主导地位 当 Claude Code 确实选择推荐工具时,它的偏好非常明确: ...

Git的魔法配置文件

Git 支持许多配置文件来变更其行为,这篇博客详细介绍了各种配置文件的作用。 必备配置:.gitignore 用的最多,甚至可以说每个项目必备。定义哪些文件不应该被 Git 跟踪。 其他实用配置 .gitattributes 配置 Git 如何处理特定文件,如行尾归一化、二进制文件标记、自定义 diff 驱动等。 .lfsconfig Git LFS 的配置文件,让团队可以使用统一的 LFS 设置。 .gitmodules 子模块配置文件,用于管理嵌套的 Git 仓库依赖。 .mailmap 邮箱映射,解决 contributors 更换邮箱或姓名后显示为多个人的问题——再也不会出现两个我了。 .gitmessage 配置提交消息的模板。但需要每次 clone 后手动运行 git config commit.template .gitmessage,所以不是常规的选择,大多数项目更喜欢用 commit-msg hooks。 参考: Git’s Magic Files - nesbitt.io 本文包含AI生成内容

Zvec:阿里巴巴开源的轻量级进程内向量数据库

阿里巴巴开源的 Zvec 向量数据库是一个轻量级、快速的进程内向量数据库,为开发者提供了一个简单而强大的方式来构建向量搜索应用。在 AI 和向量搜索技术快速发展的今天,我们来深入了解这个值得关注的项目。 什么是 Zvec? Zvec 是一个开源的进程内向量数据库,主打"轻量级、闪电般快速"。与传统的独立向量数据库服务不同,Zvec 采用进程内架构,可以直接嵌入到应用程序中运行。它基于阿里巴巴经过实战检验的 Proxima 向量搜索引擎构建,继承了阿里巴巴在高并发、大规模场景下的技术积累。 核心特性 极快的速度:能够在毫秒级别搜索数十亿个向量,性能表现优异 开箱即用:安装后几秒钟即可开始使用,无需复杂的服务器配置 多种向量支持:同时支持密集向量(dense vectors)和稀疏向量(sparse vectors) 混合搜索:可以结合语义相似度和结构化过滤条件进行精确搜索 跨平台运行:作为进程内库,可以在笔记本、服务器、CLI 工具甚至边缘设备上运行 数据持久化:支持本地文件存储,数据不会因为进程退出而丢失 快速上手 Zvec 目前支持 Python 和 Node.js 两种语言,Python 版本支持 3.10-3.12,覆盖了主流的开发环境。 安装 1 pip install zvec 或者使用 Node.js: 1 npm install @zvec/zvec 基本使用示例 让我们通过一个完整的例子来了解 Zvec 的基本用法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import zvec # 定义集合 schema schema = zvec.CollectionSchema( name="example", vectors=zvec.VectorSchema("embedding", zvec.DataType.VECTOR_FP32, 4), ) # 创建集合(本地文件存储) collection = zvec.create_and_open(path="./zvec_example", schema=schema) # 插入文档 collection.insert([ zvec.Doc(id="doc_1", vectors={"embedding": [0.1, 0.2, 0.3, 0.4]}), zvec.Doc(id="doc_2", vectors={"embedding": [0.2, 0.3, 0.4, 0.1]}), ]) # 按向量相似度搜索 results = collection.query( zvec.VectorQuery("embedding", vector=[0.4, 0.3, 0.3, 0.1]), topk=10 ) # 结果按相关性排序 print(results) 这个简单的例子展示了 Zvec 的三个核心操作:定义 schema、插入数据和搜索查询。 ...

使用 mcporter 发现和管理 MCP 工具

最近在探索 AI 工具生态时,发现了一个很有用的工具——mcporter。这是一个专门用于 Model Context Protocol (MCP) 的 CLI 工具和生成器,可以帮助我们更方便地发现和使用各种 MCP 服务器提供的工具。 什么是 MCP? Model Context Protocol (MCP) 是一个开放标准,定义了 AI 助手如何与外部工具和服务进行通信。通过 MCP,我们可以将各种功能集成到 AI 助手中,比如文档查询、数据库访问、API 调用等。 mcporter 的核心功能 mcporter 提供了几个关键功能: 1. 服务器管理 1 2 mcporter list # 列出所有配置的 MCP 服务器 mcporter list <server> --schema # 查看特定服务器的工具定义 这个功能让我能够快速了解有哪些可用的工具,以及每个工具的输入输出格式。 2. 工具调用 1 mcporter call <selector> [key=value ...] 可以直接调用 MCP 工具,支持通过 HTTP URL 或服务器名.工具名的选择器来定位。 3. 配置管理 mcporter 会自动从 config/mcporter.json 加载服务器配置,也支持从编辑器(如 Cursor、Claude)导入配置。 实际应用场景 配置好 mcporter 后,我发现它在以下几个场景特别有用: ...

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

近期计划开这个系列的坑,内容大多都是“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, 这里也仅仅是类似于复读的"再解释”. ...