无心插柳柳成荫

因为我自己维护了一个类似 OpenRouter 这样的代理平台,所以我用各种 AI 应用的时候都会关注能否用自己的 OpenAI Compatible API。前段时间发现 GitHub Copilot 似乎不支持。 本来以为这是故意不让你通过这种方式去获取它的 prompt,后来发现它也允许本地的 ollama 服务,所以应该不是这个原因。而且没过多久又听说 GitHub Copilot 要开源了…… 后来发现了一个 Issue:Add custom OpenAI endpoint configuration (Base URL & Model Parameter) of copilot chat settings · Issue #7518 · microsoft/vscode-copilot-release 就是说的这个问题,但是似乎即便大家提供了 PR 和解法但是官方就是不采用。于是后来发现了一条 comment mock 了 ollama 的 API,我觉得这很好,符合我的“别人不改就我来 patch”的开发精神😂 其实我早就想这么干了,只是自己不用 ollama 的 API 又不想自己去读 ollama 的文档 然后我立马发了个 python 库(其实算是一个 CLI 工具),来实现这个东西,但是通过一行命令就能启动: uvx oai2ollama --api-key ... --base-url ... 这样就会在 ollama 的 11434 端口启动一个 API 服务,Chat Completions 请求会自动 rewrite 到你提供的 OpenAI Compatible API 服务去。 本来以为这就是一个自用的工具,没想到居然时不时看到有人 fork 有人 star,现在居然有 14 颗 star 了!看来果然是跟 AI 相关的东西就会有 star 涌来啊。唉(有点世风日下的感叹,当然我不是一刀切哈)面向社区的项目无人问津,面向自己的小工具反而有人迹 感觉老天在嘲笑我😭

2025/8/8
articleCard.readMore

隐患与惊喜并存的 .pth

.pth 有个饱受争议的点,就是它允许执行任意代码(参考 site 文档) .pth 是干嘛的 .pth 文件是一类文本文件,如果放在 site-packages 目录下,python 解释器在启动阶段,会读取并解释其内容。.pth的内容很简单,一行行的,每行只能是以下三者之一: 一个路径,这个路径会被添加到 sys.path 中。 有一定 python 经验的人应该都了解过 sys.path 是什么。简单来说就是加载 module 的根目录列表。 一个 import 开头的语句,这个语句会被 exec() 执行。 本质上其实允许了执行任意代码 一个 # 开头的行,就是注释,跳过。 其中这个第二点就是比较危险,比如有害的第三方库如果骗你安装了,就可以在你每次正常启动 python 时,在执行你常规代码之前,进行一些 monkey patching 的操作 当然你安装一个包当然就是信任它了,所以怪不得别人 😂 只是想提醒大家,你安装一个包之后即使不用命令行或者 import 的方式调用它,它也可能可以让你在不经意间执行它。比起更广为人知的通过 sdist 在 build 阶段执行人代码的安全问题,这个方式更鲜为人知。 但是这篇文章不是想讲这些的,而是想讲讲我发现了这个功能可以有一些很好的用法 好的用途 加载环境变量很讨厌。要么你安装 python-dotenv 然后用 dotenv run 启动你的项目,要么在某个最早调用的模块里调用 dotenv.load_dotenv(override=True),如果你只想在开发阶段加载 .env(因为生产环境一般直接在平台上设置环境变量,不通过 .env 文件),可能还得放在一个 try-expect 块中,甚至写上这样的模板代码: from contextlib import suppress with suppress(ImportError): from dotenv import load_dotenv load_dotenv(override=True) 多丑陋!而且这样会多出一个令人疑惑的 __init__.py(因为你往往希望尽可能早地进行这个调用)。在我写 hmr 之前,还涉及到一个 overhead 的问题:contextlib 不是一个很轻的标准库。 今天我写了一个超小的库:CNSeniorious000/dotenv.pth: load `.env` when python starts - GitHub 当你安装了这个库,每次你任何方式启动 python 或者用什么 cli 工具,它都会自动 load 环境变量。这意味着如果你只想在开发环境中加载 .env,你只需要把 dotenv.pth 装在 dev dependencies 中就行了。 另一个例子是,我的 hmr-daemon,它现在还没有任何文档,但我可以在这里简单说一下: 它启动一个 daemon 线程,注册 hmr 中的 ModuleFinder,使得所有当前项目的模块都是 ReactiveModule 但是所有 reload 都在这个线程中执行,所以你看到的效果就是,如果你的代码还在运行,这时候你保存其中某个文件(比如更新一个函数的实现,或者比如更改某个参数),立即就能看到效果 常用的场景是,你想用 python 的 shell(或者 ipython 之类的)来交互式地调试你的项目,但是当你修改项目文件后,你肯定懒得关闭再开启 shell 一遍。 通过 hmr-daemon,你不用重启 shell,所有 from module import value 得到的 value 都是最新的。或者 module.value 这样的也是最新的。 一个坑 开发过程中往往会在虚拟环境中开发,但是将虚拟环境的 site-packages 添加到 sys.path 中的逻辑本身也是通过一个 .pth 文件实现的,它叫 _virtualenv.pth,而 python 执行 .pth 是按字典序执行的,所以如果你的 .pth 文件以 _ 开头,就很可能没法 import 一个在虚拟环境中安装了的包,因为这时候虚拟环境可以说还没别“激活”—— 激活虚拟环境的 .pth 还没执行呢。 debug 终于发现是这个原有的时候还挺搞笑的 😭 总结一下 我认为 .pth 是个很 Pythonic 的特性,很鲜明地展现了 Python 的个性 —— 极高的自定义性。可以用它实现很多提高 DX(developer experience)的小功能。 而且我认为这个东西并没有被人开发得很完全,我想我可能未来也会开发更多基于 .pth 的有意思的库。 比如我现在想到的几个是: 在 Windows 上 Python 开发有个很坑人的点,它文件的默认编码是 GBK 而不是 utf-8,所以到处需要指定。我们可以在 .pth 中 monkey patch 这个 可以监测 .env 的变化,然后随时更新到当前进程的环境变量(不过这个 hmr 本身也快要能实现了:它现在的依赖图不止支持 python 模块也支持任意文件了,正在 reactive-fs 分支积极开发中,说起来这个功能可能最早受启发于这条评论) 像 nested-asyncio 这样的“只暴露一个无参函数”的包,完全可以发布一个 .pth 包,这样不用在代码中显示地 patch,而是在依赖中这么做 …… 能想到的还有很多,但今天挺晚了,我先回去睡觉了

2025/7/17
articleCard.readMore

踩了 getsource 的坑……

整整半天 一直在 debug,一层一层往下排查…… 最后发现问题就在于 getsource() 在 Windows 上有可能即使文件已经改变了但是仍然获得旧的结果…… 排查了很久很久,最后发现在写入文件的时候,可能 mtime 的元数据不会那么快更新,或者粒度比较粗,导致修改完代码文件立刻调用 getsource 并不会触发 linecache 重新读取代码文件。也可能是 getsource 的问题 其实并没有把原因完全搞清楚,但已经完全不想管了,毕竟多一事不如少一事,而且人就是这么不求甚解,一旦发现解决问题的办法了,就可能不再追求搞懂它最深层的原因了 唉 不过一方面也是因为 python 标准库完全没有类型标注,代码写的比第三方库烂的多……看了污染眼睛 而且这个 bug 只在 Windows 上出现,而且在 GitHub Actions 上基本上每次都能复现,而我在本机上虽然也是 Windows 但基本上每次都不能复现,所以我 debug 的过程就很恶心:必须不停把更多的东西 print 出来,然后 push 到 GitHub 触发 Action…… 解决方案 from linecache import cache if filepath in cache: del cache[filepath] 相关的 commit 历史,简直是 debug 血泪史:Commits · promplate/pyth-on-line

2025/7/15
articleCard.readMore

为什么不用静态分析来实现 HMR

几个月前,我看到 How to build Hot Module Replacement in Python 这篇文章,讲的是用他们的 Tach 这个 Python 静态分析工具,可以获得一个依赖图,类似这样的键值格式: { "a.py": ["b.py", "c.py"], "b.py": ["d.py"], "d.py": ["e.py"], "c.py": ["f.py"] } 说起来,这个项目上个月开始 unmaintained 了 😅 一问才发现原来是开发者跑去 AI 创业了。我觉得网友说的挺好: I guess that's the issue with open source tools being backed by VCs. These tools will never be maintained if you can't make tons of money off of them. 然后在每次检测到某个文件更改时,依照依赖图更新(调用 import)它和它所有直接、间接影响的模块,这就时它的大致原理。 问题 Python 开发者通常不喜欢把文件分得太细,所以事实上如果按照它这套技术方案的话,往往每次重载要涉及的文件会很多(甚至任何一个文件都会让整个项目的每个模块都重载)。使用这套解决方案相比通常的冷重载,其实只省下了 import 第三方库的时间和启动 Python 本身的 overhead。这也让这个所有中间状态都丢失了。 另一方面,懒加载和动态的加载也没法通过静态分析获得。 我们的运行时方案 hmr采用截然不同的办法,在运行时记录依赖关系。与tach的静态分析的方式相反,hmr不通过 AST 去分析依赖,而是记录每次 import 了哪些 module,以及用了那个模块中的哪些变量,来决定依赖图。 举个例子,一个文件data.py如下: a = 1 b = 2 c = a + b # 3 然后有个文件a.py使用了data.py的值: from data import a print(a) 如果是静态分析,会认为a.py依赖于data.py,如果data.py重载了,则a.py也应该重载。 而我们的hmr则支持“更细粒度的反应式”,什么意思呢,它认为a.py只依赖于data.py中的a的值。所以如果这时候更改a.py的b = 2为b = 3,这时候data.a完全没有改变,所以a.py并不会重载。 只有当你修改了a = 1为别的值时,才会触发a.py重新打印 a 的值。同理,如果你的入口文件是c.py其内容如下: import data print(data.c) 那么当你改变a或者b的时候(只要a + b的结果变了)那 c 就会变,导致重新打印出data.c出来。 这就是静态分析永远难以企及的细粒度热重载。 原理 其实很简单,本质上,我只要让每次某个 module 被__getattr__了都记录下来就行了。比如,如果a模块触发了b.__getattr__("c"),意味着a.py中肯定存在from b import c或者b.c这样的地方,那么a模块就依赖于b.c。 至于如何自定义 module 的__getattr__,就是普通的元编程了(我在 这篇 文章中详述过)。简单来说就是实现一个自定义的 ModuleType 子类,然后在sys.meta_path中注册一个 ModuleFinder 就行了。 结语 经过大半年的努力,我真的处理了超级多 edge cases,解决了很多 performance issues,目的就是让所有的魔法都 work transparently,让所有的 dragon 都不用被用户影响。 现在 hmr 作为一个 CLI 已经完全模仿 python 的行为了。比如你平常以python main.py -a --b c这样启动的应用,直接改成hmr main.py -a --b c这样,所有事情应该 just work ~ 如果你用 uvicorn 启动 Web 服务,你也可以试试看 uvicorn-hmr,它也是一个 drop-in replacement of uvicorn --reload,直接替换命令即可。我在向后兼容上做的非常小心 包名同 CLI 名,比如 hmr 就是pip install hmr,uvicorn-hmr 就是pip install uvicorn-hmr 最近还做了个有趣的更 hack 的工具,hmr-daemon,它的行为是,pip install hmr-daemon后,什么代码都不用改,但是当你修改某个文件而且此时 python 进程还没退出的话,这个模块就会重载,当然它影响到的模块也会按正确的顺序重载。对于在 REPL 比如ipython中使用你项目中的模块时很有用。

2025/7/12
articleCard.readMore

给 CPython 擦屁股

6 月份我个人生活上发生了一些不幸的事情,加上期末毕业季各种事务堆得我完全透不过气,我度过了充斥着绝望、内耗、颓废的十几天,宛如十年。 纵使这期间我极其低产,我还是艰难地推进 hmr 修复了几个硬骨头 bugs。编程的时候可以短暂地让脑子转起来而且一定程度上忽视这个现实世界,这让我很舒服。 前几天参加了 AdventureX 的一个预热活动,一次在深圳开展的 BuilderUp 活动,其实就是一个无主题的创业者分享会。我本来想推销一下 hmr,正好 hmr 也算 stable 了,于是就开了有史以来最大的 PR 把 reactivity 分支(历经半年,154 次提交)合并进来了:Pull Request #319 · promplate/pyth-on-line,但是后来其实也没用上。当然活动是超级棒的,玩的很开心。久旱逢甘霖。 今天这篇随笔 就是介绍下我最新处理的的一个 bug,它源于一个 CPython 搁置的 Issue: Issue #121306 · python/cpython 是我一年前给 CPython 提的一个 Issue,大意是,在这种情况下: class _: print(a) 访问 a 并不会遵循 locals -> globals -> builtins 这样的顺序:如果 globals 不是一个纯 dict 而是一个 dict 的子类,即使 a 存在,也会访问不到(报 NameError 错误)。 虽然 core devs 的回复说,就是不让 globals 是 dict 的子类的。但事实上除了 ClassDef 中访问全局变量这一个 case 之外,其它都运行得好好的。 说起来,后来我发现其中回复我的 @gaogaotiantian 就是编程区 up 主 码农高天 我自己个人也是在很多情况下使用了这个 feature,比如我最常用的 case 就是用一个 ChainMap 作为 globals,来实现上下文管理。用于 模板渲染、monkey patching 等场景。但这些场景一般不会遇到 ClassDef 语句,所以这个 Issue 对我的影响一直蛮有限的。 直到我要在 hmr 中实现函数级热重载。 通常我们说热重载,hmr 中的 m 指的是 module,也就是说重新运行的最小单位是一个模块。但写 Python 的人比较懒,而我又总是惯着别人,on the other hand 我也喜欢炫技,所以也实现了一个比较 hacky 的函数级热重载: 它基本上表现就像用 functools.cache 装饰无参的函数 当其中访问到的全局变量 / 别的模块的变量 invalidate 时,标记为脏 当这个函数源代码改变时,标记为脏 问题就在,它需要追踪访问全局变量。这本并不难,因为我已经实现了一个 ReactiveProxy 类,会通过 __getitem__ 记录所有变量的取值,但由于 CPython 的这个 bug 存在,我没法在被我标记的函数里创建 class(更准确说,是创建 class 并在 class 的 frame 中访问全局变量)。由于我 hmr 这个库是希望做到尽可能对用户无感,所以肯定是要支持所有用法的。在函数中创建 class 显然是常见的用法。 经过漫长的思索,我发现唯一的 userland solution 就是修改 AST,比如把上面的例子编译成: class _: try: a = locals()["a"] except KeyError: try: a = globals()["a"] except KeyError: raise NameError("a") from None print(a) 换句话说,就是手动从 gloabls() 和 locals() 中拿这个值。这样确保了我们自定义的 __getitem__ 一定会被触发。 这其实是一个过度简化的例子(因为其实 print 也应该进行这样的取值。而且也要除了 locals() 和 globals() 外还应该尝试 __builtins__ ,可能还要尝试外层 frames 的闭包变量) 实现 LLM 比我更熟悉 Python 的 AST 用法。而且我需求也挺清晰,于是这部分元编程就先交给各类 vibe coding 工具来做,我提思路和小的修改,迭代一二十次差不多就成了。我又清理和重构了下,目前的核心实现大概是这样: import ast from inspect import cleandoc from typing import override class ClassTransformer(ast.NodeTransformer): @override def visit_ClassDef(self, node: ast.ClassDef): traverser = ClassBodyTransformer() node.body = [ name_lookup_function, *map(traverser.visit, node.body), ast.Delete(targets=[ast.Name(id="__name_lookup", ctx=ast.Del())]), ast.parse(f"False and ( {','.join(traverser.names)} )").body[0], ] return ast.fix_missing_locations(node) class ClassBodyTransformer(ast.NodeTransformer): def __init__(self): self.names: dict[str, None] = {} # to keep order for better readability @override def visit_Name(self, node: ast.Name): if isinstance(node.ctx, ast.Load) and node.id != "__name_lookup": self.names[node.id] = None return build_name_lookup(node.id) return node @override def visit_FunctionDef(self, node: ast.FunctionDef): node.decorator_list = [self.visit(d) for d in node.decorator_list] self.visit(node.args) if node.returns: node.returns = self.visit(node.returns) return node visit_AsyncFunctionDef = visit_FunctionDef @override def visit_Lambda(self, node: ast.Lambda): self.visit(node.args) return node def build_name_lookup(name: str) -> ast.Call: return ast.Call(func=ast.Name(id="__name_lookup"), args=[ast.Constant(value=name)]) name_lookup_function = ast.FunctionDef( name="__name_lookup", args=ast.arguments(), body=ast.parse( cleandoc(""" from inspect import currentframe from collections import ChainMap f = currentframe().f_back.f_back l, e, g, b = f.f_locals, {}, f.f_globals, f.f_builtins if freevars := f.f_code.co_freevars: freevars = {*f.f_code.co_freevars} while freevars: f = f.f_back for name in f.f_code.co_cellvars: if name in freevars.intersection(f.f_code.co_cellvars): freevars.remove(name) e[name] = f.f_locals[name] ns = ChainMap(l, e, g, b) def lookup(name): try: return ns[name] except KeyError: raise NameError(name) from None return lookup """) ).body, decorator_list=[ast.parse("lambda f: f()").body[0].value], ) 简单来说,就是首先注入一个固定的 __name_lookup 函数,首先获得 class 所在的 frame,然后依次尝试 f_locals、闭包变量、f_globals、f_builtins,并在 class 的结尾删掉这个 __name_lookup 函数。 其中比较奇怪的一点是,我用了一个 @lambda f: f() 装饰器,这个装饰器本质上是“当场执行这个函数并把结果赋值为这个函数的名字”的意思。也就是说,其实我这个 __name_lookup() 其实是一个 __name_lookup(name) 的工厂函数。为了最小化对定义域的污染,我让这个工厂函数也叫 __name_lookup() 了,但其实它所返回的 lookup(name) 函数才是最终在这个 class 的定义域里的 __name_lookup 函数的真正实现。 另一个比较奇怪的点是,我在最后构造了一个 False and (name1, name2, name3) 这样的表达式,这是为了: 如果没有直接取这些值的语句,python 就不会把这些值添加进 co_freevars 于是我们就没法进行闭包的解析(也就是 LEGB 的 E 没法正确获得) False and 是为了让后面的表达式永远不会执行(要是执行了我们 __name_lookup 这么绕一大圈就白费了) 接下来递归地把所有 ast.Name 都变成 __name_lookup(...) 这样的调用,就完成啦。编译上面的例子的结果就是: class _: @lambda f: f() def __name_lookup(): from inspect import currentframe from collections import ChainMap f = currentframe().f_back.f_back l, e, g, b = (f.f_locals, {}, f.f_globals, f.f_builtins) if (freevars := f.f_code.co_freevars): freevars = {*f.f_code.co_freevars} while freevars: f = f.f_back for name in f.f_code.co_cellvars: if name in freevars.intersection(f.f_code.co_cellvars): freevars.remove(name) e[name] = f.f_locals[name] ns = ChainMap(l, e, g, b) def lookup(name): try: return ns[name] except KeyError: raise NameError(name) from None return lookup __name_lookup('print')(__name_lookup('a')) del __name_lookup False and (print, a) 完成啦 🥂 你学会了吗 :) P.S. 本文在 2025年8月28日 修正了一下,实现了正确的 LEGB 解析。凌晨做了很多很多 debug,气死我了。其实是因为 LLM 之前写的代码有坑(而且它还挺 consistent 的,没法自己 debug)…… 总之后来终于修复啦,于是这篇文章中的后两个代码块都是更新过了的,可以直接调用~

2025/6/18
articleCard.readMore

Dependabot 不支持 YAML aliases

帮你们试过了。dependabot 的 config file 不支持 yaml 的语法糖。 今天突发奇想,想到既然dependabot.yml里那么多重复的内容,为什么不能利用一下 yaml 的独特的语法糖呢? 然后发现并不行。 sad :(

2025/5/20
articleCard.readMore

Astral 终于也来分 pyright 一杯羹了

今天发现了 https://github.com/astral-sh/ty 以前就在想,既然 ruff 肯定有一套 python AST 的工具,干嘛不也做个 type checker,今天发现这个项目了 哈哈哈 冲冲冲 相信 Astral 推出 必是精品

2025/5/10
articleCard.readMore

谈谈推理模型的局限

想象一下,现在有一位拿着纸笔的小学生和一个大学牲,站在 LLM(Lots-of Learning Mankind) Evaluation 的舞台上。 第一题,背诵快速矩阵运算 Strassen algorithm 的时间复杂度 小学生学过奥数,知道矩阵乘法是什么,推理说,矩阵乘法乘 n² 次,加 n 次,那时间复杂度就是 O(n³) 大学生学计算机的,背出了正确答案 O(N^{log₂(7)}) ≈ O(N^2.8074) 第二题,以下是两个 10×10 的矩阵,请手算其点积 …… 小学生拿起笔就一行行算了起来,写了几张纸,最后给出了正确答案 大学牲只能心算,奈何 Attention 不够,最后只好放弃,说“对不起,我不能帮你计算矩阵乘法,您可以使用 numpy 进行计算,首先如果没有安装 numpy,先在终端中输入 pip install numpy……” 上面是引入 🤣 但大概就是这个意思。 三月下旬,Anthropic 的 AI 心理学文章 Tracing the thoughts of a large language model 出来之后,大家开始反思推理模型(reasoning models)的推理内容,是否真的有用 从 benchmark 上看是非常有用的,小的推理模型能比大得多的非推理模型提高数倍的分数。加上震惊世界的 R1 告诉大家训练推理模型只要 RL 就可以了,大家对 test-time scaling 的热情一直高居不下。 QwQ 32b 自称在多项任务上超越 R1,我也很心动。但是使用下来看,其实差远了。但当时只觉得很费解,还没能解,就先把这个问题搁置了 几天前收到 GitHub Copilot 收到一封广告邮件 Which AI model should I use with GitHub Copilot,就又开始思考这个问题。回想起前段时间 Grok 3 终于上 API 了,文档里提到: When to Use Reasoning Use grok-3-mini for tasks that can benefit from logical reasoning (such as meeting scheduling or math problems). Also great for tasks that don't require deep domain knowledge about a specific subject (eg basic customer support bot). Use grok-3 for queries requiring deep domain expertise or world knowledge (eg healthcare, legal, finance). 忽然想通了。推理模型增强模型能力的方法,其中真正提高模型表现的部分,直白来说就是 按格式打草稿。举例来说, 乘法计算的时候,列竖式就是按格式打草稿 做遗传题的时候,把每个亲本和子代的基因型一个个推出来就是按格式打草稿 高中数学导数题,有的题要靠逆向去凑的,这种就要猜,然后去否定各种可能性来得到答案,这些猜的 hypotheses 就是草稿 做编程算法题的时候,确定有哪些状态,每一步状态之间怎么转换,这就是打草稿 推理模型在 <think> 的时候,真正有用的部分就是这个过程。所谓 强制模型说“Wait, ...”就能提高性能 的奇技淫巧,也就是“验算”。本质上跟上面的这几个例子是一个道理。 这种需要形式化多步推理的东西,是模型本身能力再强也只是背题。看我博客的人应该至少都上过高中,知道背题是没用的。有用的是学会什么题要用什么“格式化草稿”来推。所以高中老师经常说高考靠努力,就是因为这不管你是 671b 基座的 DeepSeek R1 还是 9b 基座的 GLM Z1,只要你愿意学怎么打草稿,你们就都在同一个起跑线上 Grok 3 系列 有一个便宜 30 倍的小的版本,更快、更便宜、更会做题。这就是小学生。小学生固然有用,但大家不能只吹小学生分多高,也要看到小学生不擅长做什么。 现在看来,小学生尤其擅长做 逻辑题、数学题、编程 这些方面,但我觉得其实还是少的。很多别的场景,其实也可以“打草稿”,但是现在模型并没有学会这些草稿。我认为这些草稿很关键。 一些进一步的猜测: R1 论文里说,他们去 RL 的时候就用了 答案正确与否、CoT 长度 这两个 reward 函数。按照我上面的比喻来看,推理内容(也就是草稿)是否按格式,是一个更好的 reward。但恐怕我这两个月没那么多时间自己推进这些了(我有毕设和比赛,还有各种活😭)

2025/4/21
articleCard.readMore

关于 feed 的一些想法

今天想到两个关于 feed 的产品的点子。 痛点是,生活中总要浏览很多“订阅”类产品。无论是我订阅的公众号、朋友圈、各种交流群、我订阅的各种 rss 源、甚至各种 app 内的推荐等等。每天都要花很多时间在刷这些东西。 不刷不行,因为可能会错过一些感兴趣的东西(可能占 5%),但是刷的时候很多时间都在重复劳动。比如等加载,比如查单词,比如手动在各种页面和app辗转。 简单来说就是“不想错过”和“刷完太花时间”的矛盾。但这个矛盾其实不必有。 因为刷的过程很大一部分是不需要脑子的。不需要脑子就以为着可以让 AI 替你做。 正好这学期在做一些通用 app 自动化(参考 AutoGLM),我也一直在生活中搜集可以自动化的一些地方(可以让 AI 代替你操作手机,帮你完成的事)。当然由于 AI 不够聪明,所以我认为只能是一些规则类的,不需要什么“主观”判断的东西。我觉得这个就是一个例子。 总的来说,就是我觉得这样的产品是完全可行且至少对我有用的(其实有点类似 iOS 的通知摘要): 帮我操作手机,定时刷消息刷朋友圈刷订阅号,把内容记录下来 基于一些模糊的规则做基本的筛选(比如不要纯广告)或分类(比如讨论什么产品的是一类,讨论什么社会事件的是一类) 对每条信息做一些浅的探索,给每条消息一些元数据。比如如果是项目可以给出 demo、仓库、文档、技术栈等。比如如果是英文可以预先帮我翻译,比如如果是开源软件可以帮我找一个最小的可运行的 snippets 这其实可以拆成两个产品来做,一个就是纯粹的自动操作手机把各类 app 变成 feed,一个就是在 feed 的基础上做处理。即使没有第一个产品第二个也能做(可以用一些现成的 feed)。

2025/4/17
articleCard.readMore

解决 Dependabot 生成 broken uv.lock 的问题

昨天我在 Dependabot 的 uv 支持的 issue 下面发了一条评论: dependabot/dependabot-core#10478 · GitHub 起因是,Dependabot 最近支持更新 uv 的 lockfile 了,但是支持的很不好,总是会给出 broken 的 lockfile,大家就都在下面抱怨。很搞笑的是,大家不知道是不是没看出来其实都是同一个问题,还是想借此机会给自己的项目打广告,很多人在评论中附上自己的项目的 CI 日志,即使这些都是重复的 我昨天的评论,就是在想,既然我本地是可以修复(重建?)这个 lock 文件的,那我为什么不能在 GitHub Actions 里运行?为什么不能在 Dependabot 打开一个 PR 的时候自动地触发修复? 然后我就跟 LLM 简要描述了下,稍加修改就成了。 name: Fix uv.lock for Dependabot on: push: branches: - "dependabot/uv/**" workflow_dispatch: jobs: update-lockfile: permissions: contents: write runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Set up Python uses: astral-sh/setup-uv@v5 with: python-version: 3.12 - name: Fix lockfile run: uv sync - name: Commit and push uv.lock if changed run: | git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' # Check if uv.lock was modified or created if git diff --quiet --exit-code uv.lock; then echo "uv.lock is up-to-date." else echo "uv.lock changed, committing updates." git add uv.lock git commit -m "fix `uv.lock` after dependency update" git push origin HEAD:${{ github.ref_name }} fi 当然,还是我一直以来的观点: LLM 辅助编码的上限是你的能力 比如我有一些碰壁的经历,让我知道: 提交到github.ref会导致一个很奇怪的 bug,GitHub 会以 Dependabot 的身份把你的那个 ref merge 过来。虽然我上面的代码中最后是提交到github.ref_name,但这是我改的,LLM 生成的不是这样 LLM 忘记了permissions: { contents: write } LLM 让我在 job 里的if:语句中判断是不是 Dependabot 的请求。事实上在on:中去 filter 更好 鲁棒地编码 我觉得过了这么久,我现在发现我有一个经验,就是尽量把代码解耦合,就能让 debug 容易很多。 比如,我有的代码,给每个幂等的函数结果都用 diskcache 缓存下来,这样调试的时候就会快很多,也会便宜很多(当代码需要一些LLM请求时)。这也使得复现更容易了。 当然,用 hmr 会更高效,因为它的 invalidate 是 on-demand 的,而且不涉及重启进程。不过 hmr 0.3 我引入了一些 bug 到现在还没修复 哈哈哈

2025/4/13
articleCard.readMore

Don't Repeat Yourself

Don't repeat yourself (DRY) 原则是一项主张,支持在代码出现重复的时候就进行封装,增加复用。我本人对这个观点是非常支持和贯彻的,甚至到了一种,我对它的副作用深有体会的地步。那先说说它有什么副作用: 很可能为了复用需要写更多的代码。比如为了复用一个一行的算式,花了三四行新建了一个有三四个参数的函数,代码量不减反增 过早优化。很可能封装完之后很快又删了 / Ctrl Z 回去了,白白浪费时间 有时封装过多会造成 debug 更困难 那它有什么好处呢?除了大多数时候能精简代码,我认为还有一个很重要的点,其实是不用担心同步的问题。一处更改,每个用到的地方都会同步。 以 bump version 为例 一个例子是指定 version。不像在 JavaScript 中,可以直接通过import { version } from "../package.json"来获得包的版本,Python 中要让版本号能在pyproject.toml中有,同时在代码中能读取略微麻烦一些。可能需要花几行配置,或者自己用十来行代码来实现。 有的人可能就会觉得,比起搞这些,更愿意每次 bump version 的时候手动改这两个地方。单纯从数量上来看,每次 bump version 也就改动一个字符。避免在pyproject.toml和代码中同时改也就让每次减少了一个字符。如果为此花了 4 行代码的话,假设行 10 个字符,那要发 40 个版本才能赚回来。 但我认为,事实上 repeating yourself 的坏处不只是要敲更多的代码,而且在整个链路上(review、blame、debug……)每个步骤都更麻烦。 这些心智代价是每次 bump version 都要担负的。所以相当于有O(n)的复杂度。而配置自动同步这两者,是O(1)的。这就是我的理由。 lockfile 的例子 我的项目有一点和主流唱反调的地方,就是我不喜欢把 lockfile 提交到仓库中。尽管 lockfile 纳入 version control 算是行业共识了,我还是不喜欢这样。我直观地有这些理由: 很大,以至于仓库的大小简直就是 lockfile 的大小 diff 很多,直接让每个 PR 的大小没法直观地通过行数来看了。而且有时候会 conflict 会搞乱 GitHub 的 Inspect 页面中对不同贡献者的对比,因为一个 lockfile 往往比整个项目的其它代码都多好几倍。我看过一些项目,Dependabot 排在维护者头像的前面。因为它对仓库的贡献(由行数统计)更大 某些包管理器(比如 pdm)主张平台特定的 lockfile,我不敢苟同 还有一个和 DRY 原则类似的原因: 可能会忘记更新 lockfile,所以造成了 O(1) 的认知复杂度 当然哈,其实这些问题肯定都有解决方案,比如可以手动去掉 lockfile 再统计行数,比如 lockfile 的更新可以放到 pre-commit 中去自动化。但是终究还是不方便。最后 我选择信任。我纳入为 dependencies 的项目一定都是我选择出来的项目,这些项目我相信他们会采用 SemVer,所以我在项目元文件(package.json / pyproject.toml / Cargo.toml)中锁定好项目依赖,就足够了 认知复杂度 突然自创了这个词,我也很惊讶哈哈。那正好就谈一谈我的这个看法吧。 举一个例子:我一直看好 Rust,是因为我觉得这样的一个底层语言才是一个成熟的底层语言。它有包管理、有高级的抽象(有 typing 的宏),使得它在项目长大的时候能维持认知复杂度 我觉得就像衡量一个算法对某个任务可不可用,可以通过时间复杂度、空间复杂度。我认为一个项目能否做下去,可以用认知复杂度。 我这里的定义就是是,衡量认知压力随项目大小的增长而增长的倍率。这个自变量和因变量都是很抽象的,所以我所说的也都是定性的结论——甚至不能算结论,只能算思考。 比如,如果某个改变,使得之后每次提交代码都要花时间一定考虑某件事,这相当于是O(n)的复杂度。一个例子是前面提到的 versioning,一个更好的例子可以是 i18n —— 一旦你的项目上了 i18n 从此你每次更新文档,都得更新每个语言的文档 如果某个改变使得之后每次提交要做更多的考虑,甚至这个考虑的增量也会随着仓库的增大而增大,那这相当于是O(n²)的复杂度。比如文档。以前端框架 Astro 为例。Astro 的文档做得很好,以至于它需要有一个文档来指导开发者维护文档。他们把这个“元文档”叫做 Astro Docs Docs (AD²) 当一个项目引入了让认知复杂度增长的改动之后,它的开发效率就会大大降低。所以,我的原则是,尽量不要在一个项目的 actively developing 阶段引入这样的改动(除非你吸引到了增长的维护者群体) 弱主张 为什么今天突然想写这个主题,因为我发现其实我有时候也会对 DRY 原则做一些妥协,他们其实是一脉相承的。DRY 像是一个“强主张”,要求永远不 repeat yourself。但我在一些场景不能拒绝 lockfile 带来的好处,比如 pyodide-minimal-reproduction 是我写给 pyodide 的一个 stackblitz reproduction template,方便人们给 pyodide 提交 bug reproduction。我希望用户在 stackblitz 的环境中尽可能快地安装依赖,所以我写了个 GitHub Action 给每个分支提交一个<原分支名>-locked分支,包含一个生成的pnpm-lock.yaml文件。 这就是我提出的弱化版 DRY 原则 —— 你不要自己 repeat yourself,但不反对让 bots 来 repeat 你。 另一个例子就是让 Dependabot / Renovate 这些 bots 来更新依赖。 其实有的时候,某个项目的依赖我不信任,或者我可能要不再维护了,我希望它在未来还能被复现,我也会按照常规的方法在仓库里维护一个 lockfile。我也深受其利。但这些项目我从内心会觉得它“脏了”,不再是那么优雅了。哈哈哈

2025/4/9
articleCard.readMore

Python venv injection

对我来说,这是很常见的一种情况: 你的一个库,要运行一些用户的 python 的代码文件。 对我个人来说,我在 3 种情况中遇到了这个问题: m 这个库,支持注册 plugin,plugin 就是在当前目录或任何sys.path中的目录创建 m/commands 文件夹,在里面的文件夹暴露一个 app。这里面的代码会被暴露为一个 entry point hmr 这个库,会运行用户的文件,其衍生品uvicorn-hmr会执行用户的某个 entry point embed 这个库,会执行用户 pull 下来的 app,这个 app 可能有单独的虚拟环境 最简单的解决办法就是读进来exec对吧,当然你有点经验的话就知道这样对 traceback 不友好。比较正确的办法是用runpy.run_path 但是还会遇到一个问题: 用户如果这个文件用到一些包,而这些包不在你安装你这个库的环境中,导致在你的库的 python 进程中会 import 失败 最好的办法当然是在用户侧解决,比如在用户的项目中安装你这个库,但是 可能这是一个开发工具,用户不太愿意往项目中引入开发依赖 终归还是得让用户多做一步 IPython 的解决方案 如果你用过 ipython,你会发现如果你在一个没有安装 ipython 的虚拟环境中运行 ipython(你在外部安装了 ipython),它照样能 import 你虚拟环境中的包,但是会有一行 warning: UserWarning: Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. 去翻源码,能看到是在 这里 警告的。会出现什么 problems 呢,如果你 import 一些非纯 python 库,确实有可能出问题。复现的方式,就是你安装 ipython 的 python minor version 和虚拟环境的 python 不一样。 源码 也给出了答案,让你能 import 虚拟环境的库是靠把虚拟环境的 site-packages 也添加到当前 python 进程的 sys.path 中解决的 我也在我的m中 实现了这个逻辑,来支持运行用户在项目中定义的 m 插件,相关代码如下: if os.getenv("VIRTUAL_ENV") and (venv_python := shutil.which("python")) and not Path(venv_python).samefile(sys.executable): from site import addsitepackages from subprocess import run site_packages: list[str] = eval(run([venv_python, "-c", "import site; print(repr(site.getsitepackages()))"], capture_output=True, text=True, check=True).stdout) path.extend(site_packages) addsitepackages(set(site_packages)) 和 IPython 有一点区别,它是把 site-packages 与 VIRTUAL_ENV 的相对路径写死了,考虑了 Windows 和 unix 的可能性。但我不喜欢这样,我是通过在 subprocess 中执行site.getsitepackages()来获得的。这样理应更鲁棒,而且我也曾经在 pdm 的源码中看到过大量类似的使用。 更好的解决方法 我不满足于实现类似 IPython 的这种 workaround。我之前在参与 embed-ai/embed 的时候就在想,其实可以在对应的虚拟环境中启动 python,然后将自己这个库加到它的虚拟环境中再启动。这样的效果应该和“在那个虚拟环境中安装这个库”是一样的(如果我们这个库和它的依赖都是纯 Python 的)。 于是我就修改了下我的代码,在这段subprocess.run中也返回一下sys.version_info,代表 python 的版本,检查如果和当前进程不符的话就用虚拟环境的 python executable 来重新执行自己。 但是这样其实启动了两次 subprocess,并不符合我追求极致性能的惯例。转念一想,其实既然都要用 subprocess 了,不如第一次也不用了,只要用户在虚拟环境中,就一定用虚拟环境的 Python if os.getenv("VIRTUAL_ENV") and (venv_python := shutil.which("python")) and str(Path(venv_python)) != sys.executable: from site import getsitepackages from subprocess import run s = set(getsitepackages()) exit( run( [ venv_python, "-c", f"import site,sys; sys.argv={['m', *sys.argv[1:]]}; sys.path.extend({[*s, str(Path(__file__, '../' * 3).resolve())]}); site.addsitepackages({s}); from m.cli.main import app; app()", ] ).returncode ) 源文件在这里:utils/inject.py · CNSeniorious000/m 隐患 这样的问题在于,执行你的库的环境可能不是安装你的库的环境。这意味着如果你这个库有非纯 Python 的依赖,就很可能会报错(除非恰好用户的虚拟环境中也装了你的这个依赖)。

2025/4/4
articleCard.readMore

关于我给博客加了 llms.txt 以及我的数字永生计划

当别人问起为什么我愿意花时间写博客,因为 我有不少奇怪的想法 想法逐一向不同人讲述,不高效,甚至不可行 在 LLM 出现之前,可能得手动构建复杂的知识图谱,才能记录下这些内容 但在现在,只要有数据了,不要求太结构化,都可以很容易地用 RAG 或者 fine tune 的方式实现数字孪生了 当然现在提到这个词,大家想到的还是数字人、元宇宙这些。我对这些不感兴趣(我对大模型视觉上的应用不太感兴趣,因为我觉得这是不能深挖的东西,或者说,它的工作流都是很浅一层——模型+UI/UX,就喂给用户,一些节点化的编辑器,也是直接给用户用)(相比之下,LLM 应用,就可以有很多层——LLM研发、LLM部署、LLM应用、应用的观测、甚至应用套应用(比如使用 OpenAI 的新 Response API / 旧 Assistant API,Anthropic 的 Citations API,Jina 的 DeepSearch API,Cohere 的 Classify API),甚至可以用户套用户(比如 Coze 这类)。另一方面我认为 LLM 才更有生产力。图像那边基本都还是 应用跟着模型研发走。 扯远了。我是想说,我说的数字孪生,和这些高概念没关系,单纯就是希望自己实现数字永生!哈哈!我是数字生命派!不装了! Wait, 不能让别人知道我已经是 AI 了。下面我要继续以人类的口吻来完成这篇文章 \\<\\/think> 前几天看到我的一个朋友 提到 他定期拉取一个开源的列表 ai.robots.txt 来屏蔽 AI 的爬虫。看来原创作者大多不希望自己的内容被 AI 爬。 不过我对这方面比较放任,一是因为我也是从业者,我自己的话我是不会拿这些没有标注的不清楚质量的文章来源不署名地直接用作训练集的。感觉这会污染模型。就像 gpt 4o 的词表里最长的是什么 给主人留下些什么吧 之类的。我的方法是,我会提一嘴,这是一篇博客文章,或者说是某某平台上的博客文章。这样模型可以在被用户要求“生成博客风格的文章”的时候用这种口吻。而在默认情况下或者要求以百科的口吻生成的时候,就不会使用这些记忆。 二是因为我有点 数据主义(这个词从《未来简史》里看来的),所以如果我能有幸成为世界的一部分,我觉得也挺好的。三是我认为只要是公开的,就没办法禁止人拿来主义。就像我一直支持 LLM 服务乃至一切服务,都应该收费,而软件都应该免费…… 一下子又跑题了,我要保持简洁的文风!! \\<\\/think> 但是大家禁 AI 其实主要是反对它爬取做训练集。而直接用来做 RAG 应该不那么受反对。 llms.txt 就是这么一个协议,它鼓励大家(像 sitemap.xml 或者 feed 那样),在网站的根目录下放一个 /llms.txt 文件,这个文件包含了当一个 agent 想要看你的内容时,你希望呈现给它的内容。 具体是怎样的协议呢。其实就这么多了。它除了说你的这个 markdown 开头必须有一个大标题之外,没有任何要求了。也没有发布 sdk 或者 api,也没有发布什么官方的 validator。反正挺佛系的。 但是居然很多 AI 网站都集成了,我感觉大家还是看好它的。况且我的网站之前也做过全文的 /feed endpoint,所以搞起来很容易。 其实 llms.txt 还有一个共识,就是 /llms.txt 放连接,而 /llms-full.txt 放全文。我当然也是两个都实现了 试用了一下 我用我的命令行工具m来试了一下,普通问答: 等我有空了我还准备做一个放在个人网站上的 chatbot,当作我的数字分身(正好这学期也在做 LLM 训练,所以可能不一定是基于 RAG 噢,可能也 fine tune 一下)应该不会晚于3月末,因为3月末我可能能去北京的北师大又去参加一个比赛,我这次没脸找上次的队友了,所以这次一个人投的,我起了个题目叫“世另我”,先在此预告一下 嘿嘿 2025年4月4日 更新 修改 了一下,为每个页面单独创建了 llms.txt 比如这一篇的 llms.txt 就是 location.href + llms.txt

2025/3/17
articleCard.readMore

快要被 createMemo 搞疯了

以前一直以为 SolidJS 的createMemo是和 Vue 的computed一样的东西,结果今天发现不是! createMemo - Basic reactivity - SolidDocs computed - Reactivity API: Core - Vue.js SolidJS 的 createMemo是 eagerly evaluating 的,也就是说,它是和createEffect一类,创建即调用的。它文档里说: They are similar to derived signals in that they are reactive values that automatically re-evaluate when their dependencies change. 明明是用来替代 derived signals 的啊!这样看明明更像 effect 而不是 derived 而且一旦有 dependency 变动了,它是立即 evaluate 然后来根据 equal 与否来判断是否 invalidate 的,而不是按需来。按需来的叫createLazyMemo:createLazyMemo - @solid-primitives/memo - Solid Primitives 为什么看这个,是因为我最近在处理 hmr 库的一些奇奇怪怪的 bug。其中就在create_memo这边没转过来: 如果它是 lazily evaluate 的,那为了让各种 effect 的值保持最新,它就必须得是 eagerly invalidate 的(这也是我之前的实现)这会导致它比起普通的 signal 就丢失了在 equal 时不 notify 的能力 如果它是 eagerly evaluate 的(就像 SolidJS 这样),它确实就可以 lazily invalidate 了,因为它可以先比较是否 equal 再 notify 了,不会立马 invalidate,但这样会导致它可能会有一些不必须的 evaluate(比如可能它的 subscriber 正好已经不需要用到它了,即使这些 subscribers 重新执行了,也不会再 observe 这个 signal) 然后就觉得其实这东西是不是没有完美的解法?这是一个权衡: Effect 这边不可能不重新执行就知道是否还需要某个 Signal 一个 Derived 在被 trigger 时不可能不重新运行就知道它的结果会不会 equal 一个办法是,可以像createMemo那样,当被 trigger 后就立即 recompute,但是第一次 compute 推迟到第一次被调用时。这样相当于autorun=False的create_effect+ cache 而已。或者说这是我的底线,因为如果 API 是这样实现的,用户也可以在创建了这个 Memoized 之后立即 call 它,来达到和 SolidJS 的版本一样的效果,但是反过来不行。 所以其实可以暴露一个参数,类似 optimization: "lazy-recompute" | "lazy-invalidate" | False 其中 lazy-recompute 代表之前的行为,lazy-invalidate 是上面说的这种,False 代表不检查 刚刚想了下,其实 SolidJS 的 createMemo 等效于: class _: var = State(0) derived = State(0) def __init__(self): @create_effect def _(): self.derived = self.var * 2 # 假设就是乘二的关系 # 但是好像需要保存一个 这个 effect 的引用,不然会被 gc 掉,但这里就不展示这些细节了 而我上面提到的中间形态等效于: class _: var = State(0) _derived = State(0) @cache # 保证只运行一次,而且只在第一次需要时调用 def ensure_effect(self): @create_effect def _(): self._derived = self.var * 2 @property def derived(self): self.ensure_effect() return self._derived 而现在是这样的 API: class _: var = State(0) @memoized_property def derived(self): return self.var * 2 未来允许给这个装饰器加参数,比如memoized_property(optimization="lazy-invalidate")获得上面第二种的效果。 其实刚刚忘说了,Svelte 5 的反应式原语好像略有不同,我略微探索了一下(顺带一提,顺便不小心发现了他们一个 bug:`$derived` unexpectedly reevaluate · Issue #15414 · sveltejs/svelte),观察到: 如果某个$derived没有至少一个$effect用到它,它就不会被执行,这说明它是 lazy-evaluate 的 虽然$effect第一次执行时,是从浅到深调用$derived们的,但是每次更新最底层的$derived先调用,再往外调用的,这说明至少最外层的$effect不是 eagerly-trigger 的 所以我怀疑它是一个 2-pass 的机制,当一个 Signal notify 时,它 invalidate 了某个$derived(相当于我这里的 memo),然后一层一层向上 invalidate,但是 invalidate 并不会立即删掉原来的 cache 的值,而只是把自己 mark 为 stale 状态(或者叫 dirty 吧),这样冒泡到最浅层,这就是“正向传播” 到达最外层的时候,如果是 memo,就说明这个 memo 没被用上。(可以形象地理解为一个$effect就是在最外层“拉”一个反应性的变量的,如果没有人“拉”,那它就不会被求值。 如果是个 effect,那这个 effect 并不会立即执行,而是会看它到底被谁 trigger 了。是全是 memo 还是也有 signal。只有被 signal 触发的才说明必须要 invalidate。否则就逐个排查每个 memo,看这些 memo 是否改变了值(相当于看每个 memo 作为一个 signal 的性质),如果改变了,那这个 effect 就得 reevaluate 了 这就是 effect 的“反向传播”。那如何知道某个 memo 是否改变了呢? 如果 notify 它的 dependencies 中有 signal,那它就得 recompute,然后比较值是否改变了 如果 notify 它的 dependencies 都是 memo,那就得先看这些 memo 有没有 change 了 这就是 memo 的反向传播 (写到这里我意识到,可能叫这些 memo 为 derived 更合适一些。今后我实现的时候会注意) 这么看来,effect 具有一个反向传播性质,这个确实我还没 implement,而且似乎 SolidJS 之类的也没这样的机制。我之前参考的更多是我见识到的 SolidJS 的一小部分,所以设计还很不成熟。 这么看,其实反应式编程有三种角色: Signal,具有 track 跟踪调用者(自动绑定 subscribers)、notify(正向传播)的功能 Effect,具有 invalidate(被 notify 时调用)、check/determine(还没想好叫什么名字,总之就是invalidate后会调用,反向传播)的功能 Derived,具有以上二者的性质,Effect 是正向传播的终点,而 Signal 其实是反向传播的终点 这么看这和神经网络太像了!这不就是输入层、输出层和中间层嘛!! 原谅我写文章总是这么意识流。今后有机会肯定会写篇系统的文章跟大家讲讲的!现在如果有人对反应式编程感兴趣的话,我还是会推荐 Ta 去读读 Vue 的这篇文章: Reactivity in Depth - Vue.js 的前三小节: What is Reactivity? How Reactivity Works? (原标题是 How Reactivity Works in Vue,但其实讲的是 in JavaScript 而非局限于 Vue) Runtime vs. Compile-time Reactivity 这篇文章讲的真的很中肯,提到了反应式编程的鼻祖,也讲到了新潮,而且说到了 why 和 why not,看得出来每个设计都是非常深思熟虑的结果。

2025/3/2
articleCard.readMore

linter docs 作为训练集

刚刚想到,像 Ruff 这样的 linter 对每条 rule 都给出了很详细的文档,其实可以作为很好的 SFT 和 DPO 源。准备试验一下 作为 SFT,可以这么构造训练集: 输入描述和 do not use,叫模型输出 use 输入描述和案例,叫模型输出 risks 输入 do not use 和 use,叫模型输出 preference 和 reason 输入 do not use 和“请改进”之类的提示词,叫模型输出 reason 和 use,或者 reason 也作为输出 作为 DPO,可以这么构造训练集: 输入代码的用途解释(这个需要生成),对比 do not use 和 use 对比模型生成的解释和官方的解释 我准备有空开个仓库,构建个工作流来做这事儿,定期将 ruff 的 docs 生成训练集,发布到 huggingface 上,以及自己微调几个模型试试看 初步成果: ruff rule --all --output-format json 这样能获得一个大的 json list,schema 大概是这样: class Rule(TypedDict): name: str code: str linter: str summary: str message_formats: list[str] fix: str explanation: str preview: bool 其中只有 explanation 我们比较有用,这是个 markdown 格式的,我试着解析了下: 解析了 ruff 的所有 rules 的 explanation 的 markdown 为结构化的数据,可以看到,就是这三个字段: 是什么 What it does 为什么 Why is this bad 怎么办 Example、Use instead 接下来把这三个部分提取出来,就能作为一个数据集来训练写出好的代码的能力了。 (截至 ruff 0.9.7,一共有 915 条 rules) 还是稍微统计了一下,其实不止这三个标题,一共是 Why is this bad? (915) What it does (914) Example (831) References (517) Options (135) Fix safety (104) Examples (65) Known problems (37) Known issues (33) Formatter compatibility (16) Removed (10) Notebook behavior (6) See also (6) Typing stub files (.pyi) (5) Details (3) Note (3) Removal (3) Fix availability (3) Fix safety and availability (2) Preview (2) Error suppression (2) Use instead: (1) Examples: (1) Known deviations (1) Fix behaviour and safety (1) Preview-mode behaviour (1) What it does? (1) Limitations (1) Fix Safety (1) 看得出来他们并没有很好地控制这些。甚至有的是忘了标点符号了😂难道说 Astral 也是草台班子? 2025年2月25日 更新:https://github.com/astral-sh/ruff/pull/16364 居然给我 merge 了

2025/2/23
articleCard.readMore

关于 HMR 的实现

上期 谈到,我在 Python 实现了反应性原语,然后 又实现了 HMR,但是没细讲,这次写一下从反应性原语到 HMR 是怎么实现的。 我也新开了个 仓库,未来放 hmr 生态的东西(如果社区有反响的话),不过由于这算是临时起意中的临时起意(歪楼项目中的歪楼项目),所以代码还是在原仓库中没有拿出来。这个仓库目前就是一个 README 哈哈哈 如果想要直观理解 hmr 对 Python 到底意味着什么,可以看看 这个仓库 里的两个视频,另外我还分别给 FastAPI 和 Flask 开了 Discussion,分别也附上了精致的录屏,欢迎来下面讨论: FastAPI | Flask (我还准备给 gradio、streamlit、pytest、litestar 也提一下) 不过没时间了,明天我要回家了,所以先不急着搞了 怎么实现的 正如我仓库中所说,HMR 实现可以拆成三部分: 实现那几个反应性原语 让加载进来的包,能自动跟踪依赖 实现一个 Reactive 的 context(按 item 追踪依赖) 实现一个自定义的 ModuleType,使用这个 Reactive 的 context 实现一个 Loader 以及一个 MetaPathFinder,并添加到sys.meta_path中,这样新加载进来的模块就会用我们的机制来初始化 通过watchfiles监听 FS 变化,并 invalidate 对应的 module 其中第 3 部分正如其字面意思,很简单,就是 watchfiles 的基本用法,没什么创意。本文重点讲第二部分 Reactive namespace 我们需要实现一个 MutableMapping,类似 Vue 的 reactive,每个__getitem__会触发对应 item 的 track,而__setitem__和__delitem__会触发对应 item 的 notify。实现如下: class Reactive[K, V](Subscribable, MutableMapping[K, V]): UNSET: V = object() # type: ignore def __hash__(self): return id(self) def _null(self): return Signal(self.UNSET, self._check_equality) def __init__(self, initial: Mapping[K, V] | None = None, check_equality=True): super().__init__() self._signals = defaultdict[K, Signal[V]](self._null) if initial is None else defaultdict(self._null, {k: Signal(v, check_equality) for k, v in initial.items()}) self._check_equality = check_equality def __getitem__(self, key: K): value = self._signals[key].get() if value is self.UNSET: raise KeyError(key) return value def __setitem__(self, key: K, value: V): with Batch(): self._signals[key].set(value) self.notify() def __delitem__(self, key: K): state = self._signals[key] if state.get(track=False) is self.UNSET: raise KeyError(key) with Batch(): state.set(self.UNSET) self.notify() def __iter__(self): self.track() return iter(self._signals) def __len__(self): self.track() return len(self._signals) def __repr__(self): self.track() return repr({k: v.get() for k, v in self._signals.items()}) def items(self): self.track() return ({k: v.get() for k, v in self._signals.items()}).items() 可以看到我还多做了一些处理:比如这个 mapping 本身也是一个 Subscribable,像是对.items的使用就会订阅它的全部改动(可以这么理解:如果我直接str()了某个 module 的.__dict__,我就依赖于它的所有元素) 一些细节 exec 的时候,globals 必须是 dict 的子类,但是我们这个 Reactive 不能是 dict 的子类,因为一个 Subscribable 必须是能添加到一个 set 里的,而 set 里不能是可变元素,好像这与 dict 冲突了(所以我必须得加一个__hash__函数)。另外后来文档说 globals 甚至不能是 dict 子类,必须得是 dict 严格本身(否则我发现会有一些奇怪的问题,关于在类中的定义域啥的),详见我给 CPython 的唯一一个 Issue:121306(说起来 3.14 有望 revert 这个,不知道他们后面争论的怎么样了) 说回来,globals 必须是严格 dict,所以我们只能把这个 Reactive 作为locals传过去,但是 locals 不会继承到函数里,所以还得把对我们这个Reactive的写入,写回到这个initial里去。 class NamespaceProxy(Reactive[str, Any]): def __init__(self, initial: MutableMapping, check_equality=True): super().__init__(initial, check_equality) self._original = initial def __setitem__(self, key, value): self._original[key] = value return super().__setitem__(key, value) def __delitem__(self, key): del self._original[key] return super().__delitem__(key) Reactive module 这个就 debug 麻烦。print 的时候还会有各种循环访问的报错,总之就是很痛苦,跑通了就再也不想碰了: class ReactiveModule(ModuleType): def __init__(self, file: Path, namespace: dict, name: str, doc: str | None = None): super().__init__(name, doc) self.__is_initialized = False self.__dict__.update(namespace) self.__is_initialized = True self.__namespace = namespace self.__namespace_proxy = NamespaceProxy(namespace) self.__file = file @property def file(self): if is_called_in_this_file(): return self.__file raise AttributeError("file") @memoized_method def __load(self): code = compile(self.__file.read_text("utf-8"), str(self.__file), "exec", dont_inherit=True) exec(code, self.__namespace, self.__namespace_proxy) @property def load(self): if is_called_in_this_file(): return self.__load raise AttributeError("load") def __dir__(self): return iter(self.__namespace_proxy) def __getattribute__(self, name: str): if name == "__dict__" and self.__is_initialized: return self.__namespace return super().__getattribute__(name) def __getattr__(self, name: str): try: return self.__namespace_proxy[name] except KeyError as e: raise AttributeError(*e.args) from e def __setattr__(self, name: str, value): if is_called_in_this_file(): return super().__setattr__(name, value) self.__namespace_proxy[name] = value 这里用到一个_is_called_in_this_file,其实就是判断调用者的 frame 是不是在这个文件中: def is_called_in_this_file() -> bool: frame = currentframe() # this function assert frame is not None frame = frame.f_back # the function calling this function assert frame is not None frame = frame.f_back # the function calling the function calling this function assert frame is not None return frame.f_globals.get("__file__") == __file__ 为什么要这样,因为我这个load必须得在后面的ModuleFinder里调用,所以不能是双下划线大头的__load形式,但是如果是_load或者load都容易与模块的真实的属性冲突,所以我就做了这么一个 hack,实现了类似“package-private 级变量”的效果,我对这个还蛮洋洋得意的,哈哈哈 Module finder & loader class ReactiveModuleLoader(Loader): def __init__(self, file: Path, is_package=False): super().__init__() self._file = file self._is_package = is_package def create_module(self, spec: ModuleSpec): namespace = {"__file__": str(self._file), "__spec__": spec, "__loader__": self, "__name__": spec.name} if self._is_package: assert self._file.name == "__init__.py" namespace["__path__"] = [str(self._file.parent)] return ReactiveModule(self._file, namespace, spec.name) def exec_module(self, module: ModuleType): assert isinstance(module, ReactiveModule) module.load() class ReactiveModuleFinder(MetaPathFinder): def __init__(self, includes: Iterable[str] = ".", excludes: Iterable[str] = ()): super().__init__() self.includes = [Path(i).resolve() for i in includes] self.excludes = [Path(e).resolve() for e in excludes] def _accept(self, path: Path): return path.is_file() and not is_relative_to_any(path, self.excludes) and is_relative_to_any(path, self.includes) def find_spec(self, fullname: str, paths: Sequence[str] | None, _=None): if fullname in sys.modules: return None for p in sys.path: directory = Path(p).resolve() if directory.is_file(): continue file = directory / f"{fullname.replace('.', '/')}.py" if self._accept(file) and (paths is None or is_relative_to_any(file, paths)): return spec_from_loader(fullname, ReactiveModuleLoader(file), origin=str(file)) file = directory / f"{fullname.replace('.', '/')}/__init__.py" if self._accept(file) and (paths is None or is_relative_to_any(file, paths)): return spec_from_loader(fullname, ReactiveModuleLoader(file, is_package=True), origin=str(file), is_package=True) 很常规,很无聊 Reload 核心是我这个on_events,因为 watchfiles 的 watch 和 awatch yield 出来的 events 都长这样,就只用写一份了。 目前只考虑了 modified 事件。其他的情况是未定义的 哈哈哈 def on_events(self, events: Iterable[tuple[int, str]]): from watchfiles import Change if not events: return path2module = get_path_module_map() with batch(): for type, file in events: if type is Change.modified: path = Path(file).resolve() if path.samefile(self.entry): self.run_entry_file.invalidate() elif module := path2module.get(path): try: module.load.invalidate() except Exception as e: sys.excepthook(e.__class__, e, e.__traceback__) for module in path2module.values(): try: module.load() except Exception as e: sys.excepthook(e.__class__, e, e.__traceback__) self.run_entry_file() 这里用到一个.invalidate(),包括前面也用到一个memoized_method,这个确实还没讲,但其实就是createMemo一样的。 我这里其实是把 memoized 当“惰性的 effect”用了,可以看到,我invalidate 修改的模块,然后每个模块都 call 一下 load,保证它是最新的,其中的缓存啥的就交给机制了,测试了半天最后终于没问题了 Memoized 而其中createMemo其实就是惰性的一个derived这样的意思。我觉得 memoized 更强调了这种惰性感,所以没用 Derived 这个词: class Memoized[T](Subscribable, BaseComputation[T]): def __init__(self, fn: Callable[[], T]): super().__init__() self.fn = fn self.is_stale = True self.cached_value: T self._recompute = False def trigger(self): self.track() if self._recompute: self._recompute = False self._before() try: self.cached_value = self.fn() self.is_stale = False finally: self._after() else: self.invalidate() def __call__(self): if self.is_stale: self._recompute = True self.trigger() assert not self._recompute return self.cached_value def invalidate(self): if not self.is_stale: del self.cached_value self.is_stale = True memoized_method 其实就是在这外面包裹了一层 descriptor 而已: class MemoizedMethod[T, I]: def __init__(self, method: Callable[[I], T]): super().__init__() self.method = method self.map = WeakKeyDictionary[I, Memoized[T]]() @overload def __get__(self, instance: None, owner: type[I]) -> Self: ... @overload def __get__(self, instance: I, owner: type[I]) -> Memoized[T]: ... def __get__(self, instance: I | None, owner): if instance is None: return self if memo := self.map.get(instance): return memo self.map[instance] = memo = Memoized(partial(self.method, instance)) return memo 唉,实在没什么时间,要回去睡觉了,所以写的很匆忙,简直什么都没写,不管了 结语 正如我在那个 hmr 仓库里写的,生态最重要,我还没找 pytest(因为我不太会做 pytest plugin,所以没法给他们一个很吸引人的 demo),目前找了 FastAPI 和 Flask,其中 FastAPI 底下有个 uvicorn 的维护者评论了我几句,提醒了我我没考虑静态文件的读取: https://github.com/fastapi/fastapi/discussions/13192#discussioncomment-11796729

2025/1/10
articleCard.readMore

如何写出好代码

开启所有 lint 规则,然后一个个看过去 哈哈哈哈 夸张了,这样效率比较低。不如先老老实实把 lint 规则一条条看过去。比如 ruff 的 rules

2025/1/10
articleCard.readMore

实现了 Python 的 HMR

有点兴奋,但明天还要考试,所以不能让自己一致兴奋下去😂 我博客好像还没实现上传视频的功能(其实是语雀不让放),于是就随便传了下 b 站 <iframe src="https://player.bilibili.com/player.html?bvid=BV1TsrVYbEb7&autoplay=0" style="aspect-ratio: 33 / 20; height: auto"></iframe>

2025/1/6
articleCard.readMore

反应式编程,但是 Python

认识我的人或多或少应该都知道,我的理想之一就是把 JavaScript 的开发体验赋能 Python。marimo 做的不错,但是我这种追求细节的人不止于此,所以自己实现一套反应式编程是逃不过的(另外也是因为,搜了一圈,似乎 Python 真没这种东西) 昨天在床上一时兴起,了解了一下 signal 的这种风格的观察者模式,然后让 GPT 用 python 实现了一遍。今天考完试就立马来试了下,感觉还不错。 是不是很熟悉的感觉? 我的目的是,让使用方式尽量 pythonic,比如上面的代码中: from reactivity import State class Counter: value = State() value看起来是个类变量,但其实是个 descriptor,是类似property这样的东西。所以在类里类外都可以像访问普通的变量一样访问它,类似c.valueself.value都是可以的,而且类型检查器友好,完全泛型化。 实现如下: class State[T]: def __init__(self, initial_value: T = None): self._value: T = initial_value self.subscribers = WeakSet[Callable[[], Any]]() def get(self): for computation in _current_computations: self.subscribers.add(computation) computation.dependencies.add(self) return self._value def set(self, value: T): self._value = value if _batches: _batches[-1].callbacks.extend(self.subscribers) else: for subscriber in self.subscribers: subscriber() def __get__(self, instance, owner): return self.get() def __set__(self, instance, value: T): self.set(value) _current_computations = [] _batches = [] Computation 的实现如下: class Computation[T]: def __init__(self, fn: Callable[[], T], auto_run=True): self.fn = fn self.dependencies = WeakSet[State]() if auto_run: self() def __call__(self): self.dependencies.clear() # clear old dependencies _current_computations.append(self) value = self.fn() last = _current_computations.pop() assert last is self # sanity check return value def dispose(self): for dep in self.dependencies: dep.subscribers.remove(self) 很简单吧,但是其实没有做什么 cleanup 之类的。 然后 batch 的实现如下: class Batch: def __init__(self): self.callbacks: list[Callable] = [] def __enter__(self): _batches.append(self) def flush(self): callbacks = set(self.callbacks) self.callbacks.clear() for callback in callbacks: callback() def __exit__(self, *_): last = _batches.pop() assert last is self self.flush() 这控制了什么时候会触发回调。使用上也很方便,直接 with Batch(): ... 就好了,所有的 callbacks 会在退出这个块的时候执行。 另外,我还实现了一些函数式编程的工具,比如: from collections.abc import Callable from functools import wraps from typing import overload from .memo import Memoized, MemoizedProperty from .primitives import Batch, Computation, State def create_signal[T](initial_value: T = None) -> tuple[Callable[[], T], Callable[[T], None]]: signal = State(initial_value) return signal.get, signal.set def create_effect[T](fn: Callable[[], T], auto_run=True): return Computation(fn, auto_run) def create_memo[T](fn: Callable[[], T]): return Memoized(fn) def memoized_property[T, Self](method: Callable[[Self], T]): return MemoizedProperty(method) @overload def batch() -> Batch: ... @overload def batch[**P, T](func: Callable[P, T]) -> Callable[P, T]: ... def batch[**P, T](func: Callable[P, T] | None = None) -> Callable[P, T] | Batch: if func is not None: @wraps(func) def wrapped(*args, **kwargs): with Batch(): return func(*args, **kwargs) return wrapped return Batch() batch不仅可以with这样用,也能@batch这样作为一个装饰器来用。 留点悬念。这里面还有用到两个create_memo和memoized_property没讲

2025/1/4
articleCard.readMore

实现一个类型安全的 (f1, f2) -> f2(f1()) 函数

def compose(first, second): return lambda: second(first()) 有了这个函数,当你需要 串联 两个函数的时候,就可以很方便了,下面我举例一个使用场景 例子:序列化并用 bzip2 压缩 大家都知道 python 对象的序列化用pickle.dumps,压缩用***.compress(***替换成各种库,比如标准库的 gzip,bz2,lzma 都是这样) 前者是(Any)->bytes,后者是bytes->bytes,如果你要封装一个“序列化并压缩”的函数,你得这么写: def dumps(object: Any): serialized = pickle.dumps(object) compressed = bz2.compress(serialized) return compressed 当然这种一次性的代码一般会缩写成一行: def dumps(object: Any): return bz2.compress(pickle.dumps(object)) 再加上反序列化的代码: def loads(data: bytes): return pickle.loads(bz2.decompress(data)) 这样,再加上空行,相当于你代码加了8行。不能忍吧 用我们刚刚的compose函数,只要这样就行了: dumps = compose(pickle.dumps, bz2.compress) loads = compose(bz2.decompress, pickle.loads) 只要两行,而且清清楚楚。 但是类型呢? 我们刚刚的compress函数,没有类型注解,你可能觉得这很简单,这样就行了: def compose(first: Callable, second: Callable): return lambda: second(first()) 这样只能确保输入compose的两个参数都是可调用对象,但是并没有保证: 第二个函数可以接受第一个函数的返回值为参数 compose后的函数的返回值与second的返回值相同 我们希望h()的类型推断是Literal[42]或者int对吧 泛型 Python 有勉强够用的泛型支持,而且早在 Python 3.12 就开始支持 typescript 式的更方便的声明泛型的方式: PEP 695: Type Parameter Syntax —— What's New In Python 3.12 这么写就可以了: def compose[T1, T2](first: Callable[[], T1], second: Callable[[T1], T2]): return lambda: second(first()) 你看我甚至返回值都不用标,pyright能推算出来返回的是Callable[[], T2] 你看,这样错误就能检查出来啦☀️ 修复了g之后,类型也正确推断啦: 如果 first()有参数呢 上面的这个例子用途不够广泛,因为它用于无参调用。而有时候我很需要这个 compose 出来的函数,仍然有着原来的 first 函数的参数定义 其实我们需要的就是一个“输入 first 的参数,返回 second 的返回值”这么一个函数对吧,那就简单了: def compose[T1, T2, **P](first: Callable[P, T1], second: Callable[[T1], T2]) -> Callable[P, T2]: return lambda *args, **kwargs: second(first(*args, **kwargs)) 我只是把Callable[[], T1]改成了Callable[P, T1],加上明确最后返回Callable[P, T2]就完事儿啦。 当然,如果你不喜欢 lambda,你可以这样写一个函数,这样甚至能少写-> Callable[P, T2]这个返回值声明 右边的返回值声明是它自己生成的,这个显示是 IDE 的 inlay hint,不是我打上去的 这样你就可以让这个 compose 出的函数具有和first一样的入参签名啦: 这个例子其实挺没用的,但是我一时间想不到好例子。总之使用场景就是,可能前面那个函数有很多参数,而且你是会用到这些参数的。 后话:为什么做这个 因为我想要一个类型安全的json.dumps和json.loads。原本的json.dumps不是(Any)->str嘛,我希望是(Input)->str,而原本的json.loads不是(str|bytes)->Any嘛,我要的是(str|bytes)->Output,其中Input和Output是我两个预先定义好的TypedDict 其中 loads 很简单,直接用它的validate_json就好了: 这个入参签名和补全,太完美了 输出类型也正常 本来以为dumps也可以这么简单 但是pydantic的 TypeAdapter 实例的 dump_json 居然是返回 bytes 的!居然不是和 BaseModel 一样地返回 str!简直是大坑 不过我刚刚翻了下文档,其实有提到的: 那就只好自己转成 str 咯,虽然就是.decode()的事情,但我希望它仍然是一个dumps()函数的形式,而且我在不同场景下需要不同的dumps的参数,最后写成了这样: dumps = compose(Output.dump_json, bytes.decode) 完美~

2024/12/29
articleCard.readMore

给博客加了个 giscus

giscus 是一个评论系统,拿 GitHub Discussion 当 CMS,只要提供一个仓库,一个 discussion 的 category,就能直接获得一个评论区,超方便 但是也有不方便的地方,就是它需要填很多参数,其中有的参数我觉得是完全没必要的,比如既要 repo 又要 repoId,纪要 category 又要 categoryId,这个 id 不好拿,印象中得 API 才能拿得到,或者在 giscus 上拿 我觉得这个应该作为选填,它 API 后端如果必须用这个 id 的话就去请求 API 去 fetch 了缓存下来,不然这非常违反我“永远不在仓库里放生成的东西”的原则 不过还是妥协了,我觉得我太画地为牢了,自己给自己设限,况且这就是两个 id 的事儿,我就硬写到仓库里了,也没用环境变量 另一个问题,它的参数都是 ="1" ="0" 这样的,既不是 true 和 false,也不是 control="control" 这样 html 原生式的,就很难受。还有就是,它只有 web component,也不能 SSR,我觉得体验还是不那么好的。但是要什么自行车,方便就行了。而且正好它有个叫 borderless 的主题,配色和我这个博客挺能配合的,就用起来吧 欢迎大家在评论区跟我互动哈~

2024/12/28
articleCard.readMore

Python 读取 jsonl 的最佳实践

发现一个有趣的结论。 在 Python 中读取 jsonl 文件,读取为一个 list[dict],最佳实践是怎么样? 刚学 python 的人会这么写: def read_jsonl(jsonl_path: str): content = Path(jsonl_path).read_text() results = [] for i in content.splitlines(): results.append(json.loads(i)) return results 这个人写的已经够好了,如果是学业不精的人还能写出更千奇百怪的写法,比如用 while 找\n,或者用 range 来迭代,然后用索引来访问一个个元素什么的。如果我是老师,我觉得上面这个已经可以打 60 分了。加上它用 Path 而不是 open 我觉得可以额外加 10 分。 但熟悉 micro optimization 的人一眼就能看出各种问题,比如存在不应该的数据复制、列表不如迭代器、append 和 json.loads 反复地对象取属性,优化之后大概长这样: def read_jsonl(jsonl_path: str): with open(jsonl_path) as f: return list(map(json.loads, f)) 这样写感觉很 pythonic,比如用到了 python 中的上下文管理器,用到了 file 对象是可以按行迭代的迭代器,用到了 map 函数式编程等特性。可读性也 up up up 如果一个学生写到这种程度,我觉得已经可以打 100 分了,甚至还有点炫技的嫌疑,我可能会让 Ta 上台来给大家讲一讲 哈哈哈 但是! 这不是最快的办法。假设你只是读一个几 MB 大的 jsonl 数据文件,不关心内存(几 MB 也没啥好优化内存的),这样是最快的 def read_jsonl(path: str): content = Path(jsonl_path).read_text().rstrip().replace("\n", ",") return json.loads(f"[{content}]") 看懂了吗?是不是很 hack,这其实是把整个 jsonl 用字符串替换的方式变成一个 json 列表,然后直接一次性 loads 快多少 在我的电脑上,一个 3MB 的 jsonl 文件,上面两种办法分别是 49ms 和 42ms,后面这种 hack 是 19ms 可以认为是从 3 帧的卡顿变成了 1 帧,这个优化效果还是非常惊艳的 为什么 Python 性能之禅,能避免用 python 就避免用 python(比如避免循环),即使这会让你的代码更绕弯路 本故事源于我前天开的一个 PR: 思考题 如果是大文件呢?比如 GB 级的,肯定不能一次性读到内存中的 参考答案 我觉得得分类讨论,如果是每行都很大的,比如一行都是 MB 级的,那可能可以一行一行做: def iter_jsonl_data(jsonl_path: str): with open(jsonl_path) as f: yield from map(json.loads, f) 但如果是行数很多,但每行都不大的情况(比如日志流),那我觉得就麻烦了,可能得分 chunk,然后每个 chunk 内用上面第三种方法来 hack 当然,这些方法都是戴着镣铐跳舞,如果条件允许,最好的办法当然是拿低级语言写个 pyd import 进来,效率绝对比这些都快 这个故事还是教学意义比实践意义大

2024/12/11
articleCard.readMore

准备做一些 MCP 相关的项目

AI#DEA 主办方的一个朋友一直在安利 MCP,今天我也终于按耐不住了。试了下 嗯。很简单。老早,GPT 插件的时候就在想,为什么只有 web 上的 OpenAPI 可以。对吧,我希望本机也可以。至少要能 call localhost,但是当时技术力还不行,这事儿就搁置了。 而且 specification 这东西还真是。大厂推就才可能推成。coze 可能,logfire 可能,但如果我做 Prompt Hub 就做不起来。这一年要说我学会了什么,我学会了什么事儿做不成 哈哈哈

2024/12/9
articleCard.readMore

OOP 何必是 COP

tl;dr: 面向对象编程(Object-oriented programming)不必是面向类编程(Class-oriented programming) 像 JavaScript 这种,声明对象比声明类还方便的语言中,何必非要定义一个类,再定义实例呢? 从“和面向过程相对”的意义上说,“面向对象”指的是,封装的时候,去封装一个名词而不是封装一个动词。至于实现上,是去实现一个类,还是实现一个接口,其实都是 OOP 经常看到言论说 JavaScript 正在远离 OOP,事实上我觉得被远离的只是“OOP原教旨主义”,Java 之流那种 什么意思呢,我觉得 interface 就是定义了一个对象应该是什么 shape,比如,如果我是一个瞎子,只靠声音认狗狗🐕,只要你能汪汪叫就算你是狗 U•ェ•*U 那传统的继承式 OOP 就要求你老老实实界门纲目科属种,你得是继承了动物界、脊椎动物门、哺乳动物纲、食肉目、犬科、犬属,但接口式的 OOP 只要求你能狗叫就行了,管你是 京中有善口技者,还是音乐播放器,你都是狗。基于接口的 OOP 很显然就是更进步的、实用主义的 OOP interface DogLike { bark: () => "Woff" }; function getDog(): DogLike { return { bark() { return "Woff" } } } const dog = getDog() dog.bark() 这是面向过程吗?这也是面向对象。只要你在使用的时候不是在使用 doBark(dog) 而是 dog.bark() 你就是在 OOP 了。不需要用 class 只要你是在操作一个主体的方法,就是在 OOP 相对的,像这种 class Dog { bark(): "Woff" }; function doBark(dog: Dog) { return dog.bark() } dog = Dog() doBark(dog) 这种才是披着面向对象外皮的面向过程编程。 是的,在我的定义中,Rust 也可以是 OOP。 说起来,前两天我一个给 Gradio 的 PR 被合了: 这个 PR 也挺好玩的,起因是我发现 Gradio 5 实现热重载了,我一直挺关心热重载的,包括我 py3.online 上也自己实现了一套热重载。我看它代码时发现它的方法差不多(都是在一个模块的 __dict__ 中 exec 它的 sources),还支持一个 NO_RELOAD 的常量,if gr.NO_RELOAD 块中可以执行一些不想被热重载重新执行的代码。我觉得还蛮有意思的。然后我就在想:这个值是怎么做到动态的呢?是在热重载的时候直接改这个值,从 True 改成 False 吗?那如果人家不是用 gr.NO_RELOAD 而是直接用 from gradio import NO_RELOAD 这样的方式导入的呢?这样不就失去响应性了吗? 后来一读源码,发现更离谱,它甚至不是动态的 bool,而是通过 AST 搜索 if gr.NO_RELOAD 这样的出现,然后把整个 if 语句都删掉。我觉得这也太 hack 了,因为这导致这个 if 语句没法用 and / or 之类的结构,也没法用别名,甚至 else 子句也删了,也没法把这个 bool 当参数传给别处。而且这些,在文档里都没有描述。害人不浅啊(具体的坏处我 PR 里描述的更清楚) 于是,我的实现,简单来说就是把它编程一个伪 bool,让它的 __bool__ 是动态的就行了。简单的话直接用 list 之类的东西就行,我是继承了 bool 类(简单来说是这样,但是 Python 里 bool 不让继承,所以其实我继承的是 int)(插播一条冷知识,Python 中 bool 也是 int 的子类哦),虽然我这样也 hack,但我至少不会造成奇奇怪怪的 bug class DynamicBoolean(int): def __init__(self, value: int): self.value = bool(value) def __bool__(self): return self.value def set(self, value: int): self.value = bool(value) 本来以为他们会不会对这个命名有异议的,但是居然没有。看来他们对贡献挺不 opinionated 的,毕竟也是一个 utils.py 文件 1500+ 行的项目。哈哈哈

2024/12/8
articleCard.readMore

m ask 支持 markdown 流式渲染了

发现原来 rich 有可以用来实现流式 markdown 渲染的工具,于是就集成了一下,下面是个例子,在命令行中直接对 ruff 的一条 lint 规则进行提问 但是超出高度会闪烁,于是手动实现了下 truncate 的策略 class TruncatedMarkdown(Markdown): def __rich_console__(self, console, options): results = list(super().__rich_console__(console, options)) height = console.height count = 0 buffer = [] for segment in reversed(results): count += segment.text.count("\n") # type: ignore if count > height: break buffer.append(segment) yield from reversed(buffer) 其实 rich.live 的用法还是昨晚从 pydantic-ai 学到的: Stream markdown - PydanticAI

2024/12/5
articleCard.readMore

再谈一谈 Svelte 5

Svelte 5 发布也有一段时间了,我存量项目太多,迁移一个项目至少要半小时起步,于是就打算先缓缓。 同时我也发现了不少 Svelte 5 难用的地方,比如它实打实地在一些小的地方更繁琐了,没得洗。 就好比,要求你每个 python 项目都必须 fully typed 才能运行,这显然是在小项目上降低了开发效率的。Python 加类型还能让 LLM 来完成,但是就我的体验来看,现在 LLM 自动补全 Svelte 5 的用法 完完全全 都是错的。完全是 hallucination。 所以,当我写 Svelte 5 代码的时候,我好像回到了两年前,没有 Copilot 的时代。 昨天提了一个问题,如何实现自动 infer 组件传给调用者的 slot 的参数的类型 sveltejs/svelte/discussions/14521,维护者告诉我没有好办法。我承认像 React 那样用 typescript 的 spread 等方法来参数化组件是好办法,而且是维护大的项目的必经之路,但是在小项目(特别是对于我这种小项目巨多的人)来说就很难受了。在 Python 中这是一个 Tab 的事情,但在 Svelte 5 中,我要花 10 倍的时间。 曾经,我在反思,为什么我比起(自称)性能更佳的 SolidJs 我更喜欢 Svelte,答案是它的开发体验。它的开发效率。现在这层光环没了,我很失望。 With more and more people building more and bigger applications with Svelte, the limitations of some of our original design decisions started to become more apparent. For example, in Svelte 4, reactivity is driven entirely by the compiler. If you change a single property of a reactive object in Svelte 4, the entire object is invalidated, because that’s all the compiler can realistically do. Meanwhile, other frameworks have adopted fine-grained reactivity based on signals, leapfrogging Svelte’s performance. —— Svelte 5 is alive Svelte 原本采用的是我尤其欣赏的“即使编译器不够好,但是我仍然不让你写一点 React 一般的 boilerplate”这样的执着。它可能不是最快的,但它是最有想法的(正如我对我自己的要求一样)。所以它不仅是我趁手的兵器,更是我的座右铭一般的存在(和 Rust 在我心目中的地位平起平坐)。现在感觉被它被刺了一刀 哈哈哈 我曾经四处向人推荐 Svelte,现在只想无条件推荐 Svelte 4 了,至少我本人更喜欢 Svelte 4 的精神 当然,现在这样的发展也证明了当今技术的发展还没能让编译器聪明到这种程度,Svelte<5 算是一种尝试,现在他们决定 move on 了。getting over it 了,也不错。我可能也不会在怀旧服待太久吧。 最后重申一下,我的观点是,Svelte 5 绝对在效率、上限上都是比 Svelte 4 高的。唯一可惜的是,门槛提高了,另一方面编码效率一定程度下降了,尤其小项目的编码效率大幅度下降。

2024/12/4
articleCard.readMore

一言不合就 fork

作为急急急急急国王,我遇到用不顺手的库我就 fork!甚至再发一个包!licence 什么的都不管!我是开源带恶人 html2text 一个纯 python 包手动上传,还忘了 wheel distribution,提了 issue 没人管!果断 fork 了 CI 什么的全部都现代化,发了个 html2text2。两周前原仓库突然有了三个动态,但是也是说了要干又没干(诶,好像和我很像,惭愧惭愧) pythonmonkey 就是非不打包 wheels,我也是直接 fork,这次直接叫 pythonmonkey-fork solid-markdown 一个版本的 bug 一直不修复,甚至前两天也有人来催了。但这个我倒是没另外发包,因为 JavaScript 生态有 pkg.pr.new 可以用,不用单独发包也能直接用上 PR 分支的 build 产物 之所以想起这件事儿,是因为昨天遇到 diskcache 的 typing 的问题,决定彻底修一下,后来发了个 diskcache2,像这样的 overloads 写了三百多行,太可怕了。不过好在效果很舒服,基本上可以把 Cache 对象当 MutableMapping 用了

2024/11/30
articleCard.readMore

感觉 gpt 的 vision 能力不太行

对 gpt 的 vision 能力持怀疑态度了。 最近有在做毕设项目(前段时间经理了很严重的怠惰期),需要用到对 PDF 的 RAG,我想在这方面尝试一点彻底的东西,于是就准备用 LLM 过一遍抽取出来的内容(相当于重新排版),然后解析 markdown 的 AST 来做 chunking,还能加上目录信息来实现 contextual embedding 文本的效果很好,非常好,但是图片有问题。似乎 PDF 抽取之后图片摆放的位置是页的开头还是结尾,总之不是中间,所以需要 LLM 来插入。另外似乎图片都没有 alt 之类的东西,所以只有个序号,如果要 LLM 来排的话肯定得把内容显示给它看。 于是写了一套 vision 的 utility,最后的格式是,![]() 紧接着就会自动插入图片。 但是效果非常不满意。 我一直 fail to 让 LLM 保留所有的图片,插入的位置也不对 LLM 似乎认为图标、广告那些元素比插图更重要。我也是服了 可能是我确实在写 prompt 上低人一等吧,总之我是放弃了 加上我之前一直没干活,师兄对我应该也是没什么耐心。现在决定放弃图片了(就当是模型能力还不够) 然后刚刚在删代码,发现真的要删了好多巧思,有点伤心,就来记录一下 当然,我觉得有可能跟 gpt 对图片的 tokenizing 方式有关。一方面我没试过别的 VLM,另一方面似乎只有 gpt 着重有一章讲他们对图片的处理,就是 low 分辨率有一部分 token,然后 high 分辨率的话是分成多个 tile 这样。我不太懂模型所以不知道为什么要这么干。但我感觉这样子可能会对低分辨率的图标啊头像啊logo啊这些东西反而更关注。

2024/11/27
articleCard.readMore

Fork 了 PythonMonkey

PythonMonkey 一个多月没动了。3.13 适配遥遥无期。 另外,之前跟他们聊的去除在 post install 阶段 npm install 的事情,我也终于忍不了了 于是狠下心来 fork 了 PythonMonkey。但是我在 CodeSpaces 里发现 build 很久而且好像会失败,CI 也运行不起来(似乎他们还有一些环境变量要配置) 实现 于是就只好施展魔法了。以前都不敢想这种思路。首先看看原本的 pythonmonkey 是怎么安装的: pythonmonkey 包是 spidermonkey 的分支的 build 产物,加上一些 python 胶水和类型定义,它提供了全面的 wheel distribution 但是 pythonmonkey 要用到一些(应该主要目的是 polyfill 的)JavaScript 库,所以他们搞了一个 pminit 包,只提供 sdist,在 builld 的时候往 pythonmonkey 包的文件夹里运行 npm install 我的第一个目标就是,去掉这个 npm install 的过程,因为: 很慢,尤其是在国内,我如果不开全局代理或者用镜像源就会失败 对 nodejs 有依赖。这很可笑。凭什么我安装一个 JavaScript 运行时还需要另一个 JavaScript 运行时 看了看 pminit 的代码,发现它真的就只是执行 npm install。我决定可以直接把 node_modules 在 CI 中确定好,并单独用一个包分发。 这个不难,虽然在一些细节的抉择上花了不少时间,但总之是很简单的事。 但是由于我没法构建 pythonmonkey 啊,所以我没法去掉 pminit 的 requirements,更没法支持 3.13 了。这不是白费工夫嘛。于是我就咬咬牙,决定用 monkey patch 已有的 wheel 文件,来替换掉依赖 最后通过将近 200 行的代码,不太优雅地实现了:patch.py 还遇到了一些坑,比如: 带加号的 version 是不能上传到 PyPI 的,怪不得 torch 啊什么的得有自己的 index http1.1 有时比 http2 快,而且 http2 比 http3 也快。我怀疑是因为都是下载不多个大文件。但是其实这个结果也蛮反常的,我不太清楚为什么 Python 的 ZipFile 可以以 append 模式打开 zip,但是只能 append,不能 update,另外 TarFile 二者都不能 Windows 下的 tar 命令和 Linux 上的还蛮兼容的,但是 .tar.gz 文件都不能 append 或 update 很奇怪的是,我用 zip 命令打的 zip 包比原本的 whl 文件大。不过没研究了,可能是带了一些元数据吧 结果 发布了 pythonmonkey_node_modules,只有 2 MB 左右 发布了 pythonmonkey-fork,甚至支持了 3.13(通过 patch pythonmonkey.io 上的 nightly release) 一套 CI,可以很方便地 patch 新版本,或者更新 JavaScript 依赖 接下来准备把各种 JavaScript 库引入 python 了! 首当其冲的是把 readability 加到 m 里,甚至可以联系上翻译或者是 ask 的内容。也可以复用一部分 llm-web-reader-demo 的东西。 另外,最近也一直在尝试各种 JavaScript 库能不能移植过来。以下是一些点子: 在 python 中用 unocss 来渲染邮件 在 python 中用 shiki 来高亮代码,用于渲染邮件 / 渲染 OG 图,或者给 mkdocs 做插件,支持 pygments 不支持的语言(比如 Svelte) 用 remark 做 markdown AST 解析,比如用在 pyth-on-line 中,实现 js 和 py 侧一样的 markdown AST 解析 用 showdown 或者 turndown 做 html 转 markdown,加上 python 系的 markdownify 和 html2text 搞个同台竞技(类似 markdownify-demo 这样)(甚至可以和 llm-web-reader-demo 也比一比) 说起来,刚刚打一起差点打成了仪器。不过应该没有人知道这个梗,等这个项目做成了再说吧。 有点好笑,昨天调试这个 patch 的时候可能下载了很多次,给他们刷出了 4K 的下载量 在测 HTTP/2 啊 HTTP/3 那些的时候也给 vercel 这儿的服务刷了十几 GB 的流量账单(之前的都是百 MB 级别的) Netlify 这儿的服务刷了差不多等量的……

2024/11/24
articleCard.readMore

Cloudflare Zaraz 的开源托管组件

最近决定把一些我觉得挺有意思的文章翻译一下,一是自己看,二是以后跟别人讲某个观点的时候不用逼别人看英文。当然肯定不是我自己翻译的而是 gpt 翻译的 原文:Open source Managed Components for Cloudflare Zaraz —— Yo'av Moshe 在2020年初,我们尝试思考是否有一种方法可以在不减慢网站速度、不降低安全性并且不牺牲用户隐私的情况下加载互联网上的第三方工具。晚上,在扫描了数以千计的网站后,我们的答案是“嗯,算是吧”。这似乎是可能的:许多类型的第三方工具仅仅是在浏览器中收集信息,然后将其发送到远程服务器。理论上,我们可以找出它们正在收集什么,然后只需高效地收集一次,并在服务器端将其发送到他们的服务器,模仿他们的数据模式。如果我们这样做,我们可以完全地避免在网站内加载他们的JavaScript代码。这意味着不再有恶意脚本的风险,不再有性能损失,隐私问题也会减少。 但答案并不是一个明确的“是!”因为我们意识到这将会非常复杂。我们调查了主要第三方脚本的网络请求,往往显得晦涩难懂。我们为自己设定了大量工作,研究工具所发出的网络请求,试图弄清楚它们在做什么——这个参数是什么?这个网络请求何时发送?这个值是如何散列的?我们如何以更安全、可靠和高效的方式实现相同的结果?我们的团队每天都在面临这些问题。 当我们加入Cloudflare时,一切的规模都发生了变化。突然之间,我们在成千上万的网站上,每秒为超过10,000个请求提供服务。用户每天都通过我们的Discord频道、社区论坛,有时甚至直接在Twitter上与我们交流。他们的消息通常会是这样的:“你好!你能支持X吗?”Cloudflare Zaraz上线时,其库中有大约30个工具,但这个市场非常庞大,新工具不断涌现。 改变我们的信任模式 在我之前关于Zaraz如何使用Cloudflare Workers的博客文章中,我包含了一些关于Zaraz中工具集成的编写方式的例子。通常,Zaraz中的“工具”是一个准备有效负载并发送它的函数。这个函数可以返回一个东西——clientJS,即浏览器稍后会执行的JavaScript代码。我们尽力做到这样,即工具不会使用clientJS,除非真的有必要,实际上大多数Zaraz构建的工具集成都没有使用clientJS。 这运作得很好,只要我们是编写所有工具集成的人,客户信任我们会编写性能和安全的代码,并信任他们在尝试Zaraz时看到的结果。加入Cloudflare后,许多第三方工具供应商联系了我们,要求编写Zaraz集成。我们很快意识到我们的系统并没有强制执行速度和安全性——供应商可以简单地将他们旧的浏览器端JavaScript倒入我们的clientJS变量,然后说“我们有一个Cloudflare Zaraz集成!”,这完全不是我们的愿景。 我们希望第三方工具供应商能够编写他们自己的高效、安全的服务器端集成。我们希望使他们有可能以更好的方式重新构想他们的工具。我们还希望网站所有者能够了解他们网站上发生的事情,能够管理和控制它,并相信如果一个工具通过Zaraz运行,它必须是一个好的工具——这不是因为是谁编写的,而是因为它所构建的技术。我们意识到,为了实现这一点,我们需要一个新的定义第三方工具的格式。 引入Managed Components 我们开始重新思考如何编写第三方代码。今天,它是一个黑匣子——你通常会在你的网站上添加一个脚本,而你完全不知道它在做什么以及何时做。你无法正确阅读或分析缩小后的代码。你不知道它的行为是否与你在别处看到的一样。你不知道它何时会改变。如果你是网站所有者,你完全在黑暗中。 工具做很多不同的事情。简单的工具只是收集信息并将其发送到某处。通常,它们会设置一些cookies。有时,它们会在页面上安装一些事件监听器。基于小部件的工具可以真正操纵页面DOM,提供新的功能,比如社交媒体嵌入或聊天机器人。我们的新格式需要支持所有这些。 Managed Components是我们想象的在线第三方工具的未来。它为供应商提供了一个API,允许他们做比普通脚本更多的事情,包括将代码执行保持在浏览器之外。我们与供应商一起设计了这个格式,为供应商设计,同时考虑到用户的最大利益是每个人的长期最大利益。 从一开始,我们就构建Managed Components以使用基于权限的系统。我们希望提供比Zaraz今天更多的透明度。由于新API允许工具设置cookie、更改DOM或收集IP地址,所有这些能力都需要获得权限。安装在你网站上的第三方工具类似于在你的手机上安装应用程序——你会得到工具可以做什么和不能做什么的解释,并且可以在细粒度水平上允许或不允许功能。我们之前写过关于如何使用Zaraz不向Google Analytics发送IP地址,现在我们正在加倍努力朝这个方向发展。这是你的网站,这是你的决定。 每个Managed Component本质上是一个JavaScript模块。与今天不同的是,这个JavaScript代码不会被发送到浏览器。相反,它由一个Components Manager执行。这个管理器实现了组件使用的API。它调度来自浏览器的服务器端事件,为组件提供信息访问,同时保持它们的沙盒化和高效性。它处理缓存、存储等——所有这些都是为了让Managed Components能够实现其逻辑,而不必过多担心周围的环境。 一个示例分析Managed Component可能看起来像这样: export default function (manager) { manager.addEventListener("pageview", ({ context, client }) => { fetch("https://example.com/collect", { method: "POST", data: { url: context.page.url.href, userAgent: client.device.userAgent, }, }); }); } 上述组件在发生页面浏览时会收到通知,然后它会创建一些包含访问者用户代理和页面URL的有效负载,并将其作为POST请求发送到供应商的服务器。这与今天的做法非常相似,只是这不需要在浏览器中运行任何代码。 但Managed Components不仅仅是做以前可能的事情,而是更好地做到,它们还提供了戏剧性的新的功能。看看我们如何公开服务器端端点的例子: export default function (manager) { const api = manager.proxy("/api", "https://api.example.com"); const assets = manager.serve("/assets", "assets"); const ping = manager.route("/ping", (request) => new Response(204)); } 这三行代码是第三方可能性的完全转变。如果获得权限,它们可以代理一些内容,提供和公开它们自己的端点——所有这些都在运行网站的同一域下。如果一个工具需要进行一些处理,现在可以完全卸载浏览器,而不需强制浏览器与第三方服务器通信。 激动人心的新功能 每个第三方工具供应商都应该能够使用Managed Components API来构建其工具的更好版本。我们设计的API是全面的,对供应商的好处是巨大的: 同一域: Managed Components可以从与网站本身相同的域提供资产。这允许更快和更安全的执行,因为浏览器只需信任和与一个服务器通信,而不是许多。这也可以降低供应商的成本,因为他们的带宽将会降低。 网站范围的事件系统: Managed Components可以挂钩到网站用于跟踪事件的预先存在的事件系统。不仅不需要为你的工具提供浏览器端API,而且对用户来说更容易发送信息到你的工具,因为他们不需要学习你的方法。 服务器逻辑: Managed Components可以在与网站同一域上提供服务器端逻辑。这包括代理不同的服务器,或添加生成动态响应的端点。这里的选项是无穷无尽的,这也可以减轻供应商服务器的负载。 服务器端渲染的小部件和嵌入: 你有没有注意到,当你在网上加载一篇文章页面时,当一些YouTube或Twitter嵌入突然出现在段落之间时,内容会跳动?Managed Components提供了一个API用于注册服务器端渲染的小部件和嵌入。这意味着当页面到达浏览器时,它已经在其代码中包含了小部件。浏览器不需要与另一个服务器通信以获取一些推文信息或样式。它现在是页面的一部分,所以期待更好的CLS分数。 可靠的跨平台事件: Managed Components可以订阅客户端事件,比如点击、滚动等,而无需担心浏览器或设备支持。不仅如此——这些相同的事件将在浏览器之外也能工作——但我们稍后再谈。 预响应动作: Managed Components可以在网络响应甚至到达浏览器之前执行服务器端动作。那些动作可以访问响应对象,读取它或更改它。 集成的同意管理器支持: Managed Components是可预测的和范围界定的。Component Manager知道它们需要什么,并可以预测运行它们需要哪种同意。 正确的选择:开源 当我们开始与供应商合作为他们的工具创建Managed Component时,我们听到一个反复出现的担忧——“有什么Components Managers?这是否仅对Cloudflare Zaraz客户有用?”虽然Cloudflare Zaraz确实是一个Components Manager,并且它有一个慷慨的免费计划,我们意识到我们需要考虑得更大。我们希望使Managed Components对互联网的每个人都可用,因为我们希望整个互联网变得更好。 今天,我们宣布的不仅仅是一个新格式。 WebCM是Managed Components API的参考实现。它是一个完整的Components Manager,我们将很快发布和维护。您将能够在构建Managed Component时将其用作SDK,即使您不是Cloudflare用户,也可以在生产中使用它来加载您网站上的Managed Components。WebCM作为一个代理工作——您将其放在您网站之前,并在必要时重写您的页面并添加几个端点。这使得WebCM 100%框架无关紧要——无论您的网站在后台使用Node.js、Python还是Ruby:只要您发送HTML,它都支持。 但这还不是全部!我们还将开源一些我们自己的Managed Components。我们将一些经典的Zaraz集成转换为Managed Components,它们将很快可供您使用和改进。您将能够获取我们的Google Analytics Managed Component,例如,并使用WebCM在您的网站上运行Google Analytics,100%服务器端,而无需Cloudflare。 技术领先的供应商已经加入 在互联网上革命性地改变第三方工具是我们只能与第三方供应商一起完成的事情。我们喜欢第三方工具,并且希望它们更加流行。这就是为什么我们与一些领先公司紧密合作,创建他们自己的Managed Components。这些新的Managed Components将Zaraz的能力扩展到远远超出现在的可能性,并为这些工具的新用户提供安全和可靠的入门体验。 Drift – Drift帮助企业在最重要的时刻与客户建立联系。Drift的集成将让客户使用Drift强大的对话云——将对话营销、对话销售和对话服务整合到一个平台中——同时保持完全的沙盒化,并且无需进行第三方网络连接,从而提高用户的隐私和安全性。 CrazyEgg – Crazy Egg通过视觉热图、A/B测试、详细记录、调查等方式帮助客户改进他们的网站。网站所有者、Cloudflare和Crazy Egg都非常关心性能、安全性和隐私。Managed Components使Crazy Egg能够实现使用第三方JavaScript根本不可能的事情,这意味着我们的共同客户将获得最具性能和安全的网站优化工具之一。 我们也已经有客户渴望实现Managed Components: Hopin Quote: “我对Cloudflare的Zaraz能够将Drift的JS库移到Edge Worker并将其加载离开DOM印象深刻。由于页面加载时间的节省,我的工作更为有效。与积极寻求更好方法来增加大型MarTech堆栈的页面速度和加载时间的两家公司合作是一种乐趣。”– Sean Gowing,前端工程师,Hopin 如果您是第三方供应商,并希望加入这些技术领先的公司,请联系我们,我们会很乐意支持您编写自己的Managed Component。 Managed Components的下一步是什么 我们现在正在许多方面上开发Managed Components。在开发和维护WebCM、与供应商合作并将Managed Components集成到Cloudflare Zaraz中时,我们已经在思考未来的可能性。 我们看到未来有许多开源运行时存在于Managed Components中。也许您的基础设施不允许您使用WebCM?我们希望看到Managed Components运行时被创建为服务工作者、HTTP服务器、代理和框架插件。我们也在努力使Managed Components在移动应用中可用。我们正在努力允许在Cloudflare Zaraz上安装非官方的Managed Components。我们正在解决WWW的一个长期问题,还有很多事情要做。 我们很快将发布Managed Components的完整规格。我们也将开源WebCM,参考实现服务器,以及许多您可以自己使用的组件。如果这对您很有趣,请通过zaraz@cloudflare.com与我们联系,或加入我们的Discord。

2024/11/15
articleCard.readMore

Emoji 轮播组件

为了过几天传播学院的比赛的路演,这几天在用代码做动画 下面是临时写的一个 emoji 轮播组件: <iframe src="https://comments-generation-mock-ui.vercel.app/demo/emoji-carousel" style="height:200px"></iframe> <br> 目前是用 svelte 自带的 transition 的简单实现,但是这样性能不高,下面是一个简单的测试: <iframe src="https://comments-generation-mock-ui.vercel.app/demo/emoji-carousel/grid" style="height:330px"></iframe> <br> 组件代码: <script> import { afterUpdate, onMount } from "svelte"; import { blur, scale } from "svelte/transition"; export let emoji = ""; export let start = 0.65; export let delay = 100; export let amount = 1; let first = true; let show = false; onMount(() => { show = true; }); afterUpdate(() => { (first && emoji && show) && (first = false); }); </script> <div class="relative aspect-1 [&>*]:(absolute left-1/2 top-1/2 -translate-1/2)"> {#if emoji && show} {#key emoji} <div in:blur|global={{ delay: first ? 0 : delay, opacity: 1, amount }} out:blur={{ opacity: 1, amount }}> <div in:scale|global={{ delay: first ? 0 : delay, start }} out:scale={{ start }} class={emoji} /> </div> {/key} {/if} </div>

2024/11/12
articleCard.readMore

在 terminal 中直接提问 LLM!

最近要赶毕设开题和比赛路演的事情了,今天可能是近期最后更新一下 m 的东西。今天新加了最基本的 ask 的功能和 config 命令,长话短说,现在你可以这样使用 pip install muspi-merol[all] -U m config chat.options.model Qwen/Qwen2.5-7B-Instruct m config chat.openai_base_url https://promplate-api.free-chat.asia m config chat.openai_api_key from-blog m --help | m ask "介绍下怎么用这个 CLI" 这是个用自己解释自己的例子。 第一行是安装这个库,muspi-merol[all]会安装今天新发布的子包 mm-chat,其中目前只有 ask 这一个基本的调用 llm 的命令,目前不包含任何提示工程 后面是帮你设置了一些配置项,使用我搭建的免费 OpenAI API,使用 Qwen 2.5 7B 作为默认模型 然后把 m --help 的结果 PIPE 给 m ask,你运行下就知道是什么效果了 只要你提供 base_url 和 api_key 就可以支持任何 OpenAI API,所以 Ollama 啊 vllm 什么的本地模型都是可以直接使用的! 很多时候,用某些命令行工具遇到问题的时候,没时间看文档,只想直接解决。通常需要自己复制到某个 AI Chat 的网站去问,现在只需要 ... | m ask 就可以直接提问了

2024/11/8
articleCard.readMore

尝试双线程聊天

上周又被女朋友删了微信,后来就在 QQ 交流,直到今天才加回来。她叫我 QQ 也要保持联系,原来想还觉得挺麻烦,今天忽然发现挺好的 我以前一直觉得国内的聊天软件太初级了,居然连一个 thread 的功能都没有,想相同的人群中并发几个话题基本上不可能。我以前试过同样几个人建很多群,但是后来由于不能折叠等原因,早早就作罢了,收敛到仅剩几个群 所以结论是用群来分类是很难的,但是如果只有同质化(非命名的)多个聊天框呢?我觉得是可以尝试的。 我打算就拿我跟我女朋友做实验,试试通过两个平台同时维护两个话题是否可行。 可以想到的一个问题是,两个话题没法互相引用(微信和 QQ 的消息)、QQ 不方便转发微信公众号和小程序的内容等 但是 QQ 一个好处是它的表情比较有异域风情

2024/11/7
articleCard.readMore

m 的诞生

最近歪楼去做了一个项目,源于我英语不太好,commit message 总想不到最准确的表达,而 Copilot 不知道抽什么风,经常生成中文的 commit message,要么就是贼长,而且大小写、动词还是名词都不符合我同一个项目的写法。 于是打算自己做一个 CLI,用来生成 commit message,倒不一定要一步到位直接帮我 commit,可以写入剪贴板,感觉这是个很舒服的工作流。后来觉得这个 CLI 工具可以拓展下,加上一些我比较需要的功能。第一个想到的是 alias 的功能,以前都是用 pdm scripts,非常好用,但是现在不用慢吞吞的 pdm 改用 uv 了,而 uv 没有现在还没有 scripts 管理的功能 另外就是,确实好像没有个跨平台的 alias 工具。 于是我实现了个。就叫它 m 吧。它的配置存在三个地方,用户目录、venv 中 m 自己的文件夹下、pyproject.toml 中。这算是最佳实践了。python 中似乎没有这样的库,我打算之后完善了之后这一部分也可以单独封装个库来。下面是一些基本用法: m alias x "a b c d" 这样就设置了 x 为 a b c d 的别名。可以这样使用: m x --p v # 相当于 a b c d --p v 昨天配置了 uv workspace,第一次尝试用一个 monorepo 的架构维护 python 库哈哈哈。今天解决了一些问题,基本上 alias 的功能算是做完了。还发现了 typer 的一个 tab completion 的 bug,所以把 completion 的功能关了,这样还减少了一个依赖 哈哈哈 我目前已经加了好多 alias 了,确实都是我日常会用到的,下面是一些比较奇特的 amend 一个提交,然后强推,或者强制同步 origin HEAD 一键更新各种包管理软件和他们管理的包 ssh 到开发服务器 由于 PyPI 上短名字都被占了,我的包名还是 muspi-merl,也就是说安装略微麻烦: pip install muspi-merol 感觉有点长,不熟悉我名字的可能不好输入。我也发邮件问了自 2014 年占了茅坑不拉屎的 M 包的作者能不能白送我,目前没收到回复。等这个包变得更多功能了之后再考虑吧~ 这几天准备加上: 管理环境变量 m env set / get / del 从文件读取到剪贴板 / 从剪贴板粘贴到文件 m copy / paste chat 的功能(这也是最初做这个项目的目的) ... | m ask "" 生成 commit message m gen commit 生成 README m gen readme 发起 HTTP 请求,类似 httpx[cli],但是支持流式响应和柯里化传参(中途 input) m get / post / ...

2024/11/6
articleCard.readMore

水了两周(睡了两周)

这两周状态不太好,每天半夜才睡,中午/下午才起床,好多课都没上成。本来和几个传播学的师姐一起打了个传播学与AI交叉的创新创业比赛(ppt向的),但是总体不太顺利。希望能进复赛吧,希望能去北京路演 :) <iframe src="https://comments-generation-mock-ui-demo.vercel.app/design/right"></iframe> 这次首次尝试了用代码来做设计稿,还蛮有意思的,可能之后会把这个风格的设计用到别的项目中。 搞这个项目还跟女朋友闹了不少矛盾,正好在大家都忙的时候,搞出不少麻烦事儿。加上上上周 migrate 到 Svelte 5 的时候电脑突然坏了,耽误了好多时间,后来去小米官方维修了,把所有数据都不要了刷机,结果那次 migrate 还没 commit,无缘 Svelte 5 哈哈哈 上周似乎几乎没有什么成就。哦,做了个@mozilla/readability的 python wrapper,在 pyodide 环境就是用原生 js 运行时运行 js 代码,否则用 pythonmonkey,pythonmonkey 的 staff 似乎本来还挺有热情的,但是我那段时间很 down,似乎浪费了一次合作机会 也没时间给它做个 web demo 唉 ☀这周开始要恢复状态! 手头上有个自动化操作浏览器的项目,还有个 pdf 转 md 的项目,希望能做好。希望能这周内做好。

2024/11/4
articleCard.readMore

阅读 Svelte 5 的 migration guide

Svelte 5 migration guide • Docs • Svelte 总之,Svelte 5 的很多设计看起来是让门槛高了,但是它的好处也很直接,当项目尺寸上来了就有糖吃了。 当然也有一些我认为很不美观的,比如 另外,我是一个很喜欢编译的人,但是 Svelte 5 更多的魔法去掉了,变成了 runtime 的 Signals,我想了想,其实性能或者说编译之后的还是那样,这么看来之前的一些设计其实有一定过度设计了。 这个也太 JSX 了: Snippets instead of slots 这一段我觉得太有意思了。真的就是大大提升了一致性。这一点我以前也诟病过 就是现在感觉 Svelte markup 越来越像 JSX 了,有没有一种可能,以后直接就用 JSX 了呢? 还真是。我的编译梦破了 升级时发现 bits-ui 改了好多,唉好麻烦

2024/10/25
articleCard.readMore

useDefaultParameterLast 和 noDelete

今天尝试把 bundle 过后的丑陋代码进行格式化,当时开启了performance/noDelete和style/useDefaultParameterLast这两项,结果就出问题了,虽然具体没发现是怎么个过程,但是现在算是知道这东西不安全了

2024/10/23
articleCard.readMore

怎么用 late chunking 嵌入长文档

看了 Jina Embeddings v3: A Frontier Multilingual Embedding Model 了解到 late chunking(迟分)的技术,正好毕设这边要做论文 RAG,我觉得可以用上。 但是它一个坏处是,模型本身只能 embed 8192 tokens 的文本,所以天生地不适合做论文这种大体量的 embedding 于是我想了个办法,就是用滑动窗口。坏处就是,这样的成本会飙升,毕竟相当于 embed 每个块都要用 8192 tokens。但这不失为一种办法。而且做毕设应该不用自己掏钱吧,如果学校一毛不拔的话那不太可能。 刚刚想到一些降低成本的方式: 由于本质上都是对 markdown 进行 indexing,所以其实每个块大概是这样的: 路径信息(类似面包屑导航,某某大标题某某小标题这样)(这一部分是 sticky 的,每个 chunk 都要带上这个) 直到下一个小标题之前的所有文本 然后每个标题之间的长度可能超过 8192 可能不超过,所以不会每次都一定要顶满 8192(暂且认为只要带了标题就带上上下文了) 这是不是就是一种 contextual retrieval 了呢?如果我假设面包屑信息就是所需的上下文的话?Introducing Contextual Retrieval \ Anthropic 这么一想,觉得应该发个库,用来解决 markdown chunking 的事情。它除了用 AST 分割标题、然后插入面包屑外,还要做下面两件事情: 如果某个块还是太长,则要成小块 如果某个块太短,则相邻几个合并成大块 阈值怎么选择?我觉得和面包屑长度相比是个办法。如果比面包屑还短,那基本上就没必要单独一块了 对 这个才是最先决的条件。基本上我什么 RAG 都是先转成 markdown 再改 更何况最近也准备推一下我这个 LLM Web Reader Demo 项目

2024/10/22
articleCard.readMore

九阳与小家电

九阳股份投资价值分析报告 从上面的内容看,九阳有哪些独特优势?相对于小家电行业的其它品牌 品牌认知度:九阳作为豆浆机的发明者,成功将自身品牌与豆浆机紧密绑定,形成了强大的品牌认知度。消费者对九阳的认知主要集中在豆浆机及相关产品上,这为其在市场中建立了良好的口碑和信任度。 丰富的专利技术:九阳拥有大量的专利技术(6,521项),其中实用新型专利占比超过80%。这表明九阳在产品研发和创新方面具有较强的能力,能够不断推出符合市场需求的新产品。 研发投入:九阳的研发占比为3.5%,高于行业内其他品牌(如小熊的2.3%),并且拥有相对较高比例的研发人员(约20%)。这使得九阳在产品功能和技术上能够保持竞争优势。 轻资产运营模式:九阳的资产负债表显示其固定资产占比较小,流动资产占比高,属于轻资产运营模式。这种模式使得九阳在资金运用和风险控制上具有更大的灵活性。 多元化产品线:九阳在小家电领域的产品线覆盖广泛,包括豆浆机、电饭煲、破壁机等,形成了多元化的产品组合,能够满足不同消费者的需求。 市场拓展能力:九阳与SharkNinja的协同效应使其能够在清洁家居市场和厨房电器领域进行有效布局,借助Shark的品牌影响力和渠道优势,进一步拓展市场份额。 良好的现金流和流动性:九阳的流动资产占比较高,且现金流状况良好,这为其在市场竞争中提供了充足的资金支持,能够快速响应市场变化。 适应市场趋势的能力:九阳能够及时把握市场趋势,如疫情带来的“下厨热”,并通过产品教育和市场推广来满足消费者的需求,增强了其市场竞争力。

2024/10/19
articleCard.readMore

No. 923

居然也是 7 的倍数 哈哈哈

2024/10/18
articleCard.readMore

关于 SharedWorker 踩的坑

开发方面 console.log不方便 必须到 <chrome://inspect/#workers> 去点 inspect 才能看到它的console.log所以基本靠不了这个来 debug 了 相比之下 worker 就没问题 port不好拿 在 onconnect 时似乎有一个event.ports,但是往往只有一个 port,而且理论上就应该只有一个,搞不懂为什么是 ports 而不是 port 在 message event 中,也有个 ports 的属性,但是永远是空的,得通过event.currentTarget来拿,真够奇怪的 部署方面 不知道是不是我的情况不太一样,反正我就是没法用非 module 的方式打包,所以new SharedWorker()的时候,必须指定 { type: "module" }。另外,在vite.config.ts中也得改: export default defineConfig({ worker: { format: "es", plugins() { return [sveltekit(), dir2json({ include: ["**/*.py", "**/*.j2"] })]; }, }, }); 一是得指定 format 为 "es",二是得重新指定一遍 plugins,搞得我 debug 了好久 这个项目源于我想把 pyodide 运行在 SharedWorker 中,这样一是启动时不会 block UI 了,二是多个标签页中可以共享一个 python 实例: 我的想法是,以后允许三种方式启动 pyodide: 主线程 Worker SharedWorker 加载 会阻塞 只用等待 甚至可能不用等待 隔离性 共享 可以隔离 可以隔离但更蛮烦 可以打断 不可以 可以 可以但更麻烦 生命周期 页面 页面 不好控制 作用域 window DedicatedWorkerGlobalScope SharedWorkerGlobalScope 这么看来主要是生命周期不好管理,而且如果要隔离的话就很麻烦。以后再在这个分支上写吧 不过目前看来,除了麻烦,用 Worker 其实没什么优势 哈哈哈

2024/10/16
articleCard.readMore

Promplate 目前的成果总结

正好要给一个朋友介绍一下,于是自己就整理一下: 项目 面向 进度 介绍 promplate 开发者 stable 一个用于用到提示工程的项目的 python 库(未来可能也实现一个 JavaScript 版本的,应该不难),包含一个模板引擎、一个基于状态机的 LLM agent 框架、一个调用 OpenAI API 的 chat completions 客户端。事实上,promplate 的名字就是 prompt + template,因为这就是提示工程的核心 promplate-demo 开发者、普通用户 stable, development 一个提示工程服务端项目,本意是作为使用 promplate 做提示工程项目的一个 demo,现在作为我一个免费的简单 LLM chat 服务网站的后端。其中包含十几个 LLM 供应商的集成、一些实用的提示工程实现,比如翻译、提取、总结,以及有一个 WIP 通用领域 agent 实现 reasonify 普通用户、研究者 beta 一个非常实验性的 agent,它不生成文本,而是只生成代码。它生成的代码会立即运行,实现与用户的交互。应用场景是一些代码任务,这个项目目前完成度还不太高 partial-json-parser 开发者 stable 一个用于解析流式传输中的 JSON 的 parser,现被用于 vllm 等多个库中 partial-json-parser-js 开发者 stable 同上,但是是 JavaScript 实现的 promplate-pyodide 开发者 stable 一个对 promplate 的 monkey patch,使其能在 pyodide 环境中运行 promptools 开发者 stable 一些与 promplate 不绑定的工具函数,对 promplate 没有强制依赖 promplate-trace 开发者 unmaintained 将 promplate 与 LLM observation 平台的集成,目前支持 langsmith、langfuse,未来准备支持 literal、lunary、logfire 等,让使用 promplate 的提示工程项目可以以一行代码获得这些平台的 tracing 或者说 logging isomorphic-fetch 开发者 alpha 一个在 pyodide 和非 pyodide 环境都有一样的 API 的 http 请求库 pyth-on-line 开发者 development 我目前主要在做的项目,目标是成为 Stackblitz、Bolt.new 这样的东西 此外还有一些面向开发者的库,在这里就不提了

2024/10/10
articleCard.readMore

细粒度发包

我觉得每个包就应该 atomic,一个包就完成最小的一个任务。

2024/10/10
articleCard.readMore

无标题文档

脏数据 不完整(缺失) 噪音 不一致 数据质量的测度 准确性 accuracy 完整性 completeness 一致性 consistency 时效性 timeliness 相关性 relevance 可信性 believability 可解释性 interpretability

2024/10/8
articleCard.readMore

在考虑换一个 markdown parser 了

之前随着写前端的经验和耐心越来越多,逐渐尝试了不同的 markdown renderer: 语雀 blog 这边用的是 markdownit + highlight.js(前段时间后者改成 shiki 了) Free Chat 用的是原项目的 markdownit + highlight.js(但是是同步的) Anse 用的是 remark-rehype + rehype-prism,现在想来怪不得慢!感觉掉帧的阈值比 Free Chat 这边低一些 Promplate Docs 用的是 remark-rehype + shiki + mdsvex,由于用了 mdsvex 所以其实是渲染出 svelte 模板了,理论上灵活性最大,但既没有 markdown 的 lint 也没有 svelte 的自动补全,所以其实不太好用。更重要的是,在 Generative UI 的时代,mdsvex 这种不能流式渲染的就有硬伤了(除非接受 svelte 编译器在 runtime 运行) Pythonline 用到是 remark-parse 然后手动从 ast 渲染到 HTML,这使得有几乎足够的自定义性,可以让一些元素(inline或block的代码)具有一些响应性,另外这样不会重复修改 DOM,性能极大程度上提高了,这下基本上只有 parse 的 cost 了 自从尝试过从 AST 渲染到组件,就打算把它迁移到 Free Chat(毕竟这个项目我天天得用,有时候掉帧还是很烦的),突然想到,每种 markdown parser / renderer 肯定都可以拿到 AST 啊,何必用 remark-parse(毕竟我也没用上它什么插件,如果它性能不好的话,就没必要用它了) marked vs markdown-it vs remark vs showdown: Which is Better Markdown Parsing Libraries? (npm-compare.com) 没想到 remark 的 star 数和 download 这么少。一直以为它才是最现代的 best practice 呢

2024/9/30
articleCard.readMore

做了一个适合 Safari 的 banner 效果

发了小红书:https://www.xiaohongshu.com/discovery/item/66f96cee000000001a021da7 细节放大版:https://www.xiaohongshu.com/explore/66f971bb000000001b0203ab 灵感来源于:Banners that adapt the theme-color / X

2024/9/29
articleCard.readMore

记一个 Safari 的 bug

input 的click/select的 eventHandler 会阻止它被选中,可能是要等到这个 handler 处理完了才会被选中 发现的原因:当时在给一个<label>元素加了 ripple 的效果之后,发现 iOS 上变卡了 <input type="radio" name="model" {id} hidden on:select={() => setModel(id)} checked={$model === id}/> <label use:ripple={{ color: 'var(--c-fg-70)' }} for={id}> <h4>{name}</h4> <h5>{title}</h5> </label> 后来的解决办法,是改成用<button>,但我还是很喜欢用 label 和 input 这样,所以后来还是保留了 label 为 sr-only

2024/9/29
articleCard.readMore

“创新预算 (innovation budget)”之于我

之前一位汇丰的朋友推荐我一篇很有趣的文章 —— Choose Boring Technology(選擇無聊的技術) 今天又在一个地方看到提到这个概念,我又想起来它,于是准备写一写我的看法。我当时是这么回复他的: I like the idea that each project has "a limited number of innovation tokens to spend“. For me, however, I am more oriented towards technology rather than specific projects. So, technologies I chose are never unknown to me, and they don't waste my innovation tokens. For enterprises, I don't like competing against rival products in front of them. I always (overconfidently) believe there are currently no alternatives in the market segment where my product is positioned. 我的意思是,我自己的项目,从来不是目标导向,而是技术导向。我并不是想做什么产品,于是去学习它需要的技术,而是自己凭兴趣去学一些技术,同时想想他们能做什么项目出来。当然,这些项目可能会需要一些我还没学的技术,这时我会去判断我对那些兴趣是否感兴趣,如果不感兴趣或者太多太难或者不适合我,我就不会立即去做这些项目。 后来当我跟女朋友聊起这个观点的时候,想到,我大学时很多项目不了了之了,其实就是这个个性的必然结果。对于很多项目,我其实并没有对他们的技术那么有兴趣。在做着做着,发现我在为了这个目标,消耗了过多的 innovation tokens,超过了我的创新预算,而我有是一个不好意思跟别人说不做了的人,于是屡次发生了跑路的事件😂在此再次向大家抱歉一下~我保证在以后可能的合作中,我如果觉得做不下去了我会及时诚实相告。

2024/9/28
articleCard.readMore

Cerebras 与 Groq

<!DOCTYPE ><div typography="classic"><p><img src="https://muspimerol.site/nlark/yuque/0/2024/png/26070246/1727280704638-5d515c2d-7d78-4d92-b671-7f75487c300d.png" width="1222"></img></p><p><img src="https://muspimerol.site/nlark/yuque/0/2024/png/26070246/1727280715882-ba5b226d-077f-4039-8ab9-7a65673c1db2.png" width="1993.3333333333333"></img></p></div>

2024/9/26
articleCard.readMore

新简历 起草

我是 Muspi Merol (abbr),庄毅辉,一名北师珠的 undergraduate,一名开源爱好者 我关注 LLM 应用、开发者工具、教育、设计 (icons) 作为一个技术栈丰富,同时也有一定工作经验的个人,我的理想工作是 产品经理 / 全栈开发者 / 受赞助的全职开源者。我需要一份能维持生计的薪酬,但是氛围活泼、技术前沿,且一定程度上支持async 项目经历 为了量产提示工程项目,我有一套开源框架。我秉持 atomic 的精神,在完成主线任务的同时也让支线任务”桃李满天下“—— partial json 库 js 和 py stats 周十万下载量链接 目前正在做一个 Stackblitz + Codepen for Python 的项目。在做这个项目的过程中为 upstream 贡献了数个 PR,成为了 10K+ stars 项目的 member 在学校正在异步进行几个与生物学、社会学交叉的的项目 有一个持续一年多的半公益项目 未完待续…… 实习经历 全栈工程师、提示工程师 大三时我在上海守扣科技有一段快乐的实习经历,同事氛围轻松,工作内容开放。同时在杭州槲丰科技有一段有收获且特别的实习经历。离开上海是因为自由度不够高,且项目管理缺乏工程化。杭州的老板和工作内容在各种方面都很好,但是做的是 toB 的生意,个人不太有兴趣而且中途因为一些错误决定就退出了。也有一段在成都爱进化科技的短暂的实习经历,目前感到唯一的缺憾是同事不够年轻,后来由于个人安排暂时没有继续。 设计总监、研究助理 在大二时参加了学校里的 6+ 个宣传部门,负责平面设计、摄影等工作,有做管理的经验,有一定自媒体的经验(但个人不喜欢行业氛围)。 可以有一个向我提问的按钮,一方面用一个 Q&A 列表 RAG,另一方面可以直接发邮件通知我

2024/9/25
articleCard.readMore

OpenAI 欺骗消费者

可以看到这两个的价格是一样的,但其实上面是 gpt-4o,下面是 gpt-4o-mini In calculating-costs it is documented that: Each of those squares costs 170 tokens. Another 85 tokens are always added to the final total.” But on pricing only the gpt-4o's price is right, the gpt-4o-mini's is larger, which makes the price equals the gpt-4o's. If I can use 4o at the same price compared to 4o-mini, why am I using 4o-mini? As a consumer who has been trusted OpenAI for more than a year, I feel deceived to certain degree. 果断投诉! 没截图,但是其实即使不开{"detail": "low"}计算出来的结果也是一样的。所以我觉得要么这就是一个错误(退钱!)要么就是存心的! 计费项目 价格 / 数量 Price per 1M tokens (fixed) $5.00 Resized width 1000 Resized height 500 512 x 512 tiles 2 × 1 Total tiles 2 Base tokens 85 Tile tokens 170 × 2 = 340 Total tokens 425 Total price $0.002125

2024/9/25
articleCard.readMore

无标题文档

MPI 消息传递接口 显式通信

2024/9/23
articleCard.readMore

《数字图像处理》笔记

视觉过程 光学过程 化学过程 神经处理过程 整体视觉过程 图像成像模型 2D 亮度函数:亮度是能量的亮度 采样和量化 显示分辨率 / 观测分辨率 灰度插值 前向映射:一个失真图的像素映射到不失真图的四个像素之间,最后灰度是由许多失真图像素的贡献之和决定 前向映射是从已知图像出发的,由已知图像向未知图像的映射 后向映射:从未知图像出发 直接灰度映射(点运算) 讲按变换成 直接灰度映射是一种点操作 常见的灰度映射 图像求反 增强对比度 动态范围压缩 灰度切分

2024/9/23
articleCard.readMore

手机淋雨屏幕坏了

似乎什么都干得了,就是屏幕显示和触摸屏没反应 好烦 感觉要体验一下没有手机的日子了

2024/9/22
articleCard.readMore

《大数据分析》 笔记

特征工程 属性类型 标称属性 二元属性是特殊的标称属性 非对称二元属性指概率不同的二元属性 序数属性 相继值之间的差是未知的 数值属性 离散属性、连续属性 “无限可数”:比如学号 特征工程 特征提取 特征变换 特征选择 特征变换 z-score 标准化:均值为0,方差为1 最大最小归一化 分箱离散化 等宽分箱:“箱子的宽度”相同 等深分箱:每箱实例数称为“箱子的深度” 分箱后,用箱均值、中位数或边界值替换箱中的每个值,实现属性值的离散化 特征变换 数值化处理 有序变量 无序变量 二分类:01 多分类 one-hot 标签编码(无含义) 数据降维 主成分分析 PCA m 个 n 维数据样本矩阵 X X 的每行 零均值化 求协方差矩阵 求协方差矩阵的特征值及对应的特征向量 将特征向量按对应的特征值大小从上到下按行排列成矩阵,取前 r 行组成矩阵 即为降到 r 维后的数据 线性判别分析 LDA 有监督的线性降维 基本思想:使用数据的类别信息,将高维的样本线性投影到低维空间,使在低维空间中数据的类别区分度最大 区分度? 不同类之间的离散度 同类数据的内聚性 Lasso 降维 一种回归分析方法,通过 L1 正则化实现选择和降维 ISOMAP 流形(manifold)是一类几何对象的总称 用测地距离(geodesic distance)代替欧氏距离 算法步骤 构造近邻图 对每个节点(每个样本),构造边(k 近邻,对每个近邻点,构造一条边……最短路径长度) 计算最短路径 找出样本的低维表示 优点 保留了全局结构 只有一个参数 k 需要确定 缺点 对噪音敏感 Floyd 最短路径算法的计算代价高 特征提取 文本数据结构化 词袋法 n-gram 模型 词嵌入法 word2vec 简单的神经网络模型 连续词袋模型 CBOW:根据上下文词语预测目标词出现的概率 跳字模型 skip-gram:根据目标此预测其上下文 通过预测周围词来训练中心词的向量表示,表示能力往往更好 word2vec 缺点 在独立的局部上下文窗口上训练,难以利用单词的全局统计信息 对词是确定性的,即在不同语境中同一词的向量是一样的,这意味着 word2vec 不能解决多义词问题 特征选择 过滤式 filter —— 基于统计测试/其它单变量评估指标 包裹式 wrapper —— 于模型训练过程结合,作为评价特征子集优劣的标注(验证集?) 嵌入式 embedded —— 在模型训练过程中进行特征选择,入决策树、随机森林等 基于树模型的特征重要性 基于线性模型的特征重要性 基于互信息的特征重要性 基于 SHAP 值的特征重要性 基于随机排列的特征重要性

2024/9/20
articleCard.readMore

昨天在做爬虫

基于 playwright 的爬虫,让 LLM 来做标注 结果包含了 LLM 生成的选择器(经过一个 reflection 步骤确保了一定能解析)、选择到的元素、这些元素的 computedStyle 交集 核心代码: @main_loop.callback class _(Callback): # noqa: N801 max_retries = 4 async def end_process(self, context: ChainContext): dom: Selector = context["dom"] res: dict[str, str | None] = loads(context.result) problems = context["problems"] = [] for key, selector in res.items(): if selector: print_label(f"{key} - {selector}") try: if results := dom.css(selector): show(results) elif self.max_retries: problems.append((key, None)) else: res[key] = None except SelectorSyntaxError as e: print_exception_only(e) problems.append((key, format_exception_only(e))) if not problems or self.max_retries <= 0: raise Jump(out_of=main_loop) self.max_retries -= 1

2024/9/19
articleCard.readMore

也想做这样的图表

在 py3.online/pypi 上

2024/9/19
articleCard.readMore

取 dict 的交集

大概这样的效果: 实现: from collections import Counter def merge_computed_styles(computed_styles: list[dict[str, str]]): counter = Counter() for style in computed_styles: counter.update(style.items()) return dict([pair for pair, count in counter.items() if count == len(computed_styles)]) 在线尝试: https://bnu.py3.online/telegraph/computed-styles-09-18 <iframe src="https://bnu.py3.online/telegraph/computed-styles-09-18"></iframe>

2024/9/18
articleCard.readMore

Ruff Settings in VS Code

为了 fix on save { "[toml]": { "editor.formatOnSave": false }, "ruff.lint.select": [ "I" ], "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { "source.fixAll.ruff": "always" } } } 可以用作 py 项目的 setup template

2024/9/18
articleCard.readMore

2024 年中秋 🥮

福州之旅尾声,听说爸妈来珠海找我过中秋。于是时隔四年又去了圆明新园。 图:不知道为啥树须下面有些小揪揪。怀疑是不是先前的须根抓住了土,结果水位下降了? 晚上去了澳门环岛游 其实也蛮骗钱的。但是天气不错 沉迷长曝光 说起来我觉得这种也应该算是前景。让前景模糊不仅可以靠景深的方式,也可以靠动态的方式。两者在冥冥之中是有点联系的(一点随想) 这么看有点像火箭发射塔😃 但是颜色好脏😭 各种地标吧。也没啥意思。倒是在想什么时候上面能挂我的广告 反复拍 毕竟船开得慢,也没人聊天 这个居然自动变了 但是右上角可以看到应该是连了非内地的网络了,于是扣了我25块钱。据说是1元1MB,25元封顶送1GB。什么狗屁 笑死。明天就去投诉。我现在就是闹事先锋~ 语雀的上传图片好像不会转格式,导致我 HEIF 都没法显示。唉。早晚得做个自己的 CMS

2024/9/18
articleCard.readMore

第一次收到别人的 PR 👻

早上送走爸妈之后才发现今天没看 GitHub 通知,一打开 大部分都是垃圾信息,但是发现居然有人给我的 partial-json 提了一个 PR Add `OUTERMOST_OBJ` and `OUTERMOST_ARR` allowances by alanpog · Pull Request #9 · promplate/partial-json-parser-js Sourcery 的总结还挺好的,一看就懂 说起来,很戏剧性地,由于我这个项目是刚开始用 JavaScript 的时候写的,当时还不太懂 CI,于是没有配 formatter 的 config,这个人直接不小心把所有文件都重新格式化了,造成 600 行的 diff 😂 说回到这个功能,虽然我自己先前也想搞这么个东西,但是由于觉得想不到完美的设计,所以就没干了。 Your proposal is great and this implementation works well. But due to my perfectionist tendencies, I often find myself wondering if there's an even better way to solve these kinds of problems. 但是直接拒绝别人也不太建设性。于是我就想了很久(一个中午下午都耗在这上面了) 想了两个解决方案: Approach One — Input Predicate Function The parse will take an options object to extend configuration besides simple allow. We may support inputing a validate predicate function to choose whether to allow an object / array. The signature of options may look like this: interface options { allow: number; validate(parsed: any, text: string, parents: Parent[]): boolean } type Parent = { type: "OBJ"; key: string; } | { type: "ARR"; index: number; } For example, if user do this: parse(`[0, {"a": [{`, { allow: ALL, (parsed, text, parents) => { ... } }) The validate function will be called at most 4 times with validate({}, '{', [{ type: "ARR", index: 0}, { type: "OBJ", key: "a" }, { type: "ARR", index: 1}]) validate([{}], '[{', [{ type: "OBJ", key: "a" }, { type: "ARR", index: 1}]) validate({"a": {}}, '{"a": [{', [{ type: "ARR", index: 1}]) validate([0, {"a": {}}], '[0, {"a": [{', []) Approach Two — Return PartialInfo Inject some information into the return value. Like this: export const partial = Symbol('__partial__'); And the partial information may be like this: interface PartialInfo { text: string; } Then users can filter the result themselves using this information. For example, if using this way: > res = parse(`[0, {"a": [{`, { allow: ALL, inject: true }) // [0, {"a": [{}]}] > res[0][partial] // undefined > res[1][partial] // { text: '{"a": [{' } > res[1].a[partial] // { text: '[{' } > res[1].a[0][partial] // { text: '{' } They can drop at any depth level as they wish. 今天顺便集成了 StackBlitz 的 pkg.pr.new 的持续发布 现在每次 commit 都会可以被安装了🥳

2024/9/17
articleCard.readMore

记几个新认识的 tailwindcss 语法

偶然发现几个从没用过但有用的 tailwind 语法,记录一下: 不用[&>*]:...了,应该直接*:... 不用w-6 h-6而是应该直接size-6 可以在:root上加一个text-pretty

2024/9/16
articleCard.readMore

测试 iframe

<iframe src="//free-chat.asia"></iframe> <iframe src="//bnu.muspimerol.site"></iframe>

2024/9/14
articleCard.readMore

更舒适的 PyPI 浏览器

又花了好长时间微调 UI/UX,感觉好焦虑,明明已经时间很紧迫了,但是就是不做到完美不忍心 merge A PyPI Explorer · Pull Request #61 · promplate/pyth-on-line +376 -0又一次突破记录了😂 下一步估计得等到很后面再接着做了 [pypi] next steps · Issue #64 · promplate/pyth-on-line

2024/9/2
articleCard.readMore

最近主要在改 pyth-on-line

<console> is hard-coded, causing traceback disappear when filename specified · Issue #4901 · pyodide/pyodide 最近很喜欢 stackblitz,所以好几个 Issue 都提供了 dedicated 的复现 最近的新发现: 原来 unload 一个 module 只要把它从sys.modules里删掉就行了,我还一直想麻烦了 发现linecache可以通过exec轻松 patch,实现以下的效果 🔗 py3.online

2024/7/2
articleCard.readMore

第一次搞 polyfill,感叹 JavaScript 的 GC 真麻烦

GitHub Gist: https://gist.github.com/CNSeniorious000/9fc1a72e45358dd7c9e2f16e5d26df5c 起因是要给pyodide.http支持在 cancelled 的时候自动 abort,就需要 AbortSignal.any,结果 maintainers 之一发现这个 API 很新,甚至 FireFox 2024年才刚实现。所以叫我得 polyfill 研究了半天发现这还真不容易,而且似乎没有现成的可供 copy,只好自己实现 为此接触了 JavaScript 的WeakRef和FinalizationRegistry,还锻炼了一下写 JsDoc 的技能 想法: JavaScript 的FinalizationRegistry相比 python 的__del__还是有不方便的地方的,因为一个 registery 必须自己不被 gc 才能有用 JavaScript 里不好手动触发 gc,搞得调试比较玄学 JavaScript 里似乎即使 gc 了内存还是不会立即释放,python 里倒似乎能在任务管理器看到内存降下去 下面附上 gist 的内容: export function polyfillAbortSignalAny() { /** @param {AbortSignal[]} signals */ return (signals) => { // if (AbortSignal.any) { // return AbortSignal.any(signals); // } const controller = new AbortController(); const controllerRef = new WeakRef(controller); /** @type {[WeakRef<AbortSignal>, (() => void)][]} */ const eventListenerPairs = []; let followingCount = signals.length; /** @type {FinalizationRegistry<(callback: () => any) => void>} */ const registry = (globalThis.__abortSignalCleanups = globalThis.__abortSignalCleanups ?? new FinalizationRegistry((callback) => void callback())); signals.forEach((signal) => { const signalRef = new WeakRef(signal); function abort() { controllerRef.deref()?.abort(signalRef.deref()?.reason); } signal.addEventListener("abort", abort); eventListenerPairs.push([signalRef, abort]); registry.register(signal, () => !--followingCount && clear(), signal); }); function clear() { eventListenerPairs.forEach(([signalRef, abort]) => { const signal = signalRef.deref(); if (signal) { signal.removeEventListener("abort", abort); registry.unregister(signal); } const controller = controllerRef.deref(); if (controller) { registry.unregister(controller.signal); delete controller.signal.__controller; } console.log("clear", ++count); }); } const { signal } = controller; registry.register(signal, clear, signal); signal.addEventListener("abort", clear); registry.register(controller, () => console.log("controller", count)); signal.__controller = controller; return signal; }; } export let count = 0; // for test only // node --expose-gc ({ polyfillAbortSignalAny } = await import("./polyfill.js")); any = polyfillAbortSignalAny(); a = Array.from({ length: 2 }).map(() => { const c = new AbortController(); return [c, any([c.signal])]; }) [x, y] = a[0] x.abort() // a = a.map(([x, y]) => x) // to remove the references for followers // a = a.map(([x, y]) => y) // to remove the references for origins gc() // look at the output 好了现在更多疑点了。不知道为什么在 vitest 或者以脚本形式运行的话,就是不会触发 gc 所以总是得用命令行打 一开始是用 vitest 发现不行,又用 watchfiles 发现也不行 不过这也算是积累了小型 JavaScript 脚本的调试经验:watchfiles 比 vitest 甚至还方便 但是我 tsconfig 还是配置不对 好烦 而且这玩意算是搞了今天一天

2024/6/9
articleCard.readMore

居然一天写了 18 小时代码

<!DOCTYPE ><div typography="classic"><p><img src="https://s2.loli.net/2024/05/23/6pYaLACMcr2DmlF.png" width="1447"></img></p></div>

2024/5/23
articleCard.readMore

为什么不用 Astro

astro 我觉得好的特性是它的 island,可以融合多个生态 但是对我来说,我不需要那么多生态。但我需要差量更新和链接的预加载,因此我自己的项目不用 Astro 好的,现在发现 prefetch 其实也是有的,之前是我寡见鲜闻了。这样的话没有差量更新其实也能接受。 现在也有 type-safe 的 action 功能了,感觉确实尝试切换到 astro 了 不过其实还是有个问题:如果我的整个应用几乎每个组件都是数据驱动的,那 island 就失去它的优势了 而这种响应式 肯定是我所期望的。再考虑下切换到 astro 能给我带来的好处: 能更方便地写 markdown (但是如果 markdown 元素都得响应式就有点反而不方便了) 跨页动画用 view-transition 更优雅(但我可能希望更高的自定义程度) 总之感觉 astro 是个很好的解决方案,但确实还是不适合我😂

2024/5/13
articleCard.readMore

这个周末在抠 partial-json-parser

周五想到一种扁平的、2-pass 的实现,结果一直在 debug 扁平化的结果就是,要分类讨论很多很多情况,对我这种注意力不集中的人极其不友好🥲 我这种人大概是看不得 if-else 三层以上的算法的了😂 hypothesis 真是好啊,我准备把 js 版的也用 pythonmonkey 的 js runtime 接到 python 里,然后也用 hypothesis 来测一下。 欣赏一下 hypothesis 给出的报错: + Exception Group Traceback (most recent call last): | File "<string>", line 1, in <module> | File "D:\dev\partial-json-parser\tests\test_hypotheses.py", line 42, in main | test_partial_json() | File "D:\dev\partial-json-parser\tests\test_hypotheses.py", line 28, in test_partial_json | @given(json) | | File "D:\dev\partial-json-parser\.venv\Lib\site-packages\hypothesis\core.py", line 1638, in wrapped_test | raise the_error_hypothesis_found | ExceptionGroup: Hypothesis found 3 distinct failures. (3 sub-exceptions) +-+---------------- 1 ---------------- | Traceback (most recent call last): | File "D:\dev\partial-json-parser\tests\test_hypotheses.py", line 34, in test_partial_json | parse_json(json_string[:i]) | File "D:\dev\partial-json-parser\src\partial_json_parser\core\api.py", line 15, in parse_json | return parser(ensure_json(json_string, allow_partial, use_fast_fix)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "C:\Users\17979\AppData\Local\Programs\Python\Python312\Lib\json\__init__.py", line 346, in loads | return _default_decoder.decode(s) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "C:\Users\17979\AppData\Local\Programs\Python\Python312\Lib\json\decoder.py", line 340, in decode | raise JSONDecodeError("Extra data", s, end) | json.decoder.JSONDecodeError: Extra data: line 1 column 3 (char 2) | Falsifying example: test_partial_json( | anything=[['']], | ) | Explanation: | These lines were always and only run by failing examples: | C:\Users\17979\AppData\Local\Programs\Python\Python312\Lib\json\decoder.py:340 | D:\dev\partial-json-parser\src\partial_json_parser\core\myelin.py:124 +---------------- 2 ---------------- | Traceback (most recent call last): | File "D:\dev\partial-json-parser\tests\test_hypotheses.py", line 34, in test_partial_json | parse_json(json_string[:i]) | File "D:\dev\partial-json-parser\src\partial_json_parser\core\api.py", line 15, in parse_json | return parser(ensure_json(json_string, allow_partial, use_fast_fix)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "D:\dev\partial-json-parser\src\partial_json_parser\core\api.py", line 22, in ensure_json | head, tail = fix_fast(json_string, allow_partial) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "D:\dev\partial-json-parser\src\partial_json_parser\core\myelin.py", line 139, in fix_fast | assert last_colon < last_comma, f"\n\n {last_colon = } {last_comma = } {json_string = }" | ^^^^^^^^^^^^^^^^^^^^^^^ | AssertionError: last_colon = 3 last_comma = -1 json_string = '{"": ""' | Falsifying example: test_partial_json( | anything={'': ''}, | ) +---------------- 3 ---------------- | Traceback (most recent call last): | File "D:\dev\partial-json-parser\tests\test_hypotheses.py", line 34, in test_partial_json | parse_json(json_string[:i]) | File "D:\dev\partial-json-parser\src\partial_json_parser\core\api.py", line 15, in parse_json | return parser(ensure_json(json_string, allow_partial, use_fast_fix)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "C:\Users\17979\AppData\Local\Programs\Python\Python312\Lib\json\__init__.py", line 346, in loads | return _default_decoder.decode(s) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "C:\Users\17979\AppData\Local\Programs\Python\Python312\Lib\json\decoder.py", line 337, in decode | obj, end = self.raw_decode(s, idx=_w(s, 0).end()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "C:\Users\17979\AppData\Local\Programs\Python\Python312\Lib\json\decoder.py", line 355, in raw_decode | raise JSONDecodeError("Expecting value", s, err.value) from None | json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) | Falsifying example: test_partial_json( | anything=[''], | ) | Explanation: | These lines were always and only run by failing examples: | C:\Users\17979\AppData\Local\Programs\Python\Python312\Lib\json\decoder.py:354 | D:\dev\partial-json-parser\src\partial_json_parser\core\myelin.py:124 +------------------------------------

2024/5/12
articleCard.readMore

Agent 的最终形态 必然导向

想想,如何使得function_call的结果可以直接引用,不用LLM重述?同时又能与各家千奇百怪的LLM API的function_call格式解藕? 回忆回忆rag-compiler的方法 一边生成,一边call,再想想我在promplate-agent实现的单循环agent的parallel call 这种方式真的是具有所有好处

2024/5/10
articleCard.readMore

建了一个 PyPI 镜像

仅仅在当前安装时使用 pip install -i https://py3.online/simple 设为 pip pip config set global.index-url https://py3.online/simple 设置 PDM pdm config pypi.url https://py3.online/simple 设置 Poetry poetry source add --priority=primary mirrors https://py3.online/simple/ 起源 起初只是为了让我的 serverless 在线 python(py3.online)国内也能轻松使用上: 于是就 proxy 了一下 PyPI 和 pythonhosted 两个域名: /src/routes/simple/[[project]]/+server.ts /src/routes/packages/[...path]/+server.ts 实测速度飞快,以安装 torch 为例,速度是清华源的两倍: 但是 Netlify 的响应体上限好像有点低 又试着在别的项目也部署了下,比如 i.free-chat.asia/simple 确实都可以了

2024/4/21
articleCard.readMore

Serverless era of Python

<!DOCTYPE ><div typography="classic"><p><a href="https://blog.cloudflare.com/python-workers"><img src="https://blog.cloudflare.com/content/images/2024/04/blogpython.png" width="2000"></img></a></p><p><br></br></p></div>

2024/4/3
articleCard.readMore

看到一篇很好的讲 UX 的文章

而且非常赞同这篇文章最后说的一段话 Analyzing and making sense of design details beyond just "it feels nice" helps nurture taste, amplify level of execution, and grow appreciation for how hard the pursuit of excellence is. Invisible Details of Interaction Design 🔗

2024/4/1
articleCard.readMore

无标题文档

感觉我现在成了调包侠了

2024/4/1
articleCard.readMore

Reasonify

一个纯 function call 思路的 agent 5月9日更新 重新思考了一下标语 Real AI speaks programming languages Introducing Reasonify: The First Python Agent in the world 感觉这更像是两篇博文的标题🤔

2024/3/25
articleCard.readMore

ubuntu 配环境 🙄

无脑更新 sudo apt update && sudo apt upgrade 安装 clash git clone https://github.com/Elegycloud/clash-for-linux-backup.git 安装 python curl https://pyenv.run | bash 安装 node curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

2024/3/22
articleCard.readMore

个人简历

庄毅辉 男 汉族 2002年 大三学生,现就读于 北京师范大学(珠海校区) GitHub: Muspi Merol @CNSeniorious000 / Email: me@promplate.dev / Tel: 17620453640 目前在珠海学校里 佛系创业中 领域:LLM 相关 / Web 前后端开发 / 科班数据科学 部分技术栈 有用 JavaScript / TypeScript 开发 application 和 library 的经验,但是没有做过大项目 熟悉 serverless 平台:Vercel / Netlify / Railway 熟悉 Svelte(一款语法类 Vue 的新兴 Web 全栈框架)、SolidJS(一款语法类 React 的新兴 Web 全栈框架) 在实习公司用 NextJS 14 + LangchainJS 做过 非常善用原子化的 css 框架快速开发,自己一般用 unocss,当然 tailwindcss 也可以 测试方面用的是 vitest 一些例子:索引 / 落地页 / 博客 / 纯静态 / ChatBot 非常熟悉 python,但先不详说了 Web 开发方面熟悉 FastAPI,也用过 Django 但不太喜欢 写过 TGI / 讯飞星火 / 百度文心 转 openai 接口的 wrapper;写过 openai 的账号池中间件 了解 langchain 以及其它一些提示工程框架例如微软的 guidance 用的向量数据库主要是 chromadb,还给它写过一个小轮子 aiochroma 蛮熟悉 langsmith 的,目前已经把自己的提示工程框架和 openai 账号池对接 langsmith Web 标准方面有一定常识 熟悉 SSE(但更喜欢用 jsonl 来流式传输) 比较熟悉 PWA 但好像国内不太有什么项目用 也做过 Vanilla JS / CSS / HTML 的项目 熟悉 cloudflare;熟悉 Linux;会用 Docker 部署;会在 GitHub / Gitlab 上配流水线;熟悉日常的 git 操作 用过各种市面上常用的 LLM 的API,GPT、Claude、百度/讯飞,各种 llama Xwin 啥的 工作经历 在杭州斛丰科技做算法和后端工程师,核心负责用 python 和 JavaScript 实现推荐算法、医学问诊 Chatbot、辅助诊疗可信 AI、基于 embedding 的模糊匹配、医学领域下的类 Copilot、小规模的爬虫等项目等 在上海守扣科技做算法工程师,核心负责用 python 实现基于 chain 的医学知识图谱提取、智能陪练助理、文本格式化等开发,和一些原型项目的简单前端开发 在校期间在多个校媒任设计部部长 个人项目 做了一个专注于灵活性的提示工程框架 promplate,虽然在我的两个实习公司都用在生产环境了但其实完成度还不高,都是用到哪做到哪 更泛用的是一个 python 和 JavaScript 的 JSON 解析库,目标是用于解析 LLM 生成的 JSON。看 DEMO 就懂了 基于开源框架改写维护了两个 ChatGPT 镜像站,顺路学了不少运维等知识,维持公益运营最高时 Google Analytics 统计到 20000 用户/日 写了一些后端的东西,tgi 转 openai 接口的 wrapper,以及讯飞星火转 openai 接口的 wrapper,写过一个账号池实现,接入了 langsmith 来提供可观测性 给学校的校史写了个 documentQA,但是业余时间少,还没完成,因此就不放链接了 一个在线无需服务端的 python console,由 wasm 驱动,带有 AI 纠错等类 Copilot 功能 python 格式化工具 black 的官方 interactive playground 当然还有很多其他项目,都能在我 GitHub 上找到,但是跟 LLM 关联较大的就这些了,所以就不赘述了 东西有点多 部分内容可能已经过时了 见谅🙏

2023/11/29
articleCard.readMore

Netlify 执行 python 构建脚本比 Vercel 快

最近把本人初学 web 时的纯服务端渲染纯 html 前端的项目成功部署到了 Netlify 和 Vercel 上,对比了一下: 奇怪。一直印象中构建前端项目好像 Vercel 快一点的🤔

2023/10/12
articleCard.readMore

节后统计

kan

2023/10/9
articleCard.readMore

就msgpack压缩后大小的问题的讨论

在 GitHub 上的 msgpack/msgpack 库下面,很早之前我发起了这么一个话题,今天突然得到回复了 链接在此:https://github.com/msgpack/msgpack/issues/328 我个人得出的结论是: Length-Prefixed serialization languages is less friendly to compressors compared to Delimiter-Separated serialization languages indeed More fine-grained research can be done through taking equivalent structures in both JSON and MessagePack, compressing them both with the same algorithm, and then studying the resulting compressed encoding in detail to understand exactly what tradeoffs the compressor made Web developers should consider JSON fist now and in the future, because compressing is almost at zero cost This is not saying Length-Prefixed DML is of no use. I think its usage is representing larger data like a HEAD with 100KB body following. And the performance on speed and RAM usage is still huge advantages of Length-Prefixed DMLs Finally, I think best practice of transmitting data when pursuing ultimate size efficiency, IDLs like protobuf are what you need. IDLs remove the separator of JSON or the HEAD of basic element in MsgPack representing the basic type. If you really want self-explanatory (you want the data can be interpreted without additional type definitions), you can transmit type definition before data 😂 翻译成中文即: 压缩后,长度前缀数据的空间效率比分隔符分隔数据低 可以通过更细致的研究来进行取JSON和MessagePack中的等效结构,用相同的算法压缩它们,然后详细研究结果压缩编码,以理解压缩器做出的具体权衡。 网页开发者现在和未来都应该首先考虑JSON,因为压缩几乎是零成本 这并不是说长度前缀数据没有用。我认为它的用途是表示更大的数据,比如后面跟着100KB体的头。在速度和RAM使用上的性能仍然是长度前缀数据的巨大优势 最后,我认为在追求终极大小效率时传输数据的最佳实践,你需要的是像protobuf这样的IDLs。IDLs去除了JSON的分隔符或MsgPack中表示基本类型的基本元素的头。如果你真的想要自解释(你希望数据可以在没有额外类型定义的情况下被解释),你可以在数据之前传输类型定义😂 本文链接: https://muspimerol.site/blog/uk4tywen79xyuqam

2023/9/27
articleCard.readMore

promplate 文档规划

basic concepts promplate渐进地提高你的提示工程效率。从最简单的text complete,

2023/9/25
articleCard.readMore

为什么我要开发promplate

每一个听说我造这么一个轮子的人都会有这样的疑问: promplate 到底解决了什么问题? 在大模型和提示框架百花齐放的当下,重新造一个轮子的意义何在? 我一个一个回答。 promplate 解决了什么问题? Progressively enhance your prompting workflow. 上面这句是我为promplate整个生态撰写的slogan。什么意思呢,我希望它是一个能帮助到无论是非开发者,还是初学者,还是提示工程师的任何一个人。 对于普通人(非开发者),可以在我的在线编辑器playground上测试提示词。每个人都可以是发挥自己的创意 对于初学者,可以用极简到一个单个形参的函数直接调用我的免费API来开始提示工程之旅。换言之,0门槛 对于提示工程师,我的库具有非常大的可自定义度,它最初只是实现了我心目中的提示工程的最佳实践范式。在其上的开发和元开发都无比简单 认识我的人可能知道,我是一个对代码有着极端的洁癖的人。因此我在开发这个库的时候,是极大程度上地用效率换取对上述优势的保证。显然,要做到上面提到的那样“老少咸宜”,也就是说效率和灵活性要得兼,是非常不容易的。我通过发布一个保证灵活性、保证优雅实现、尽可能提高效率的core包,和一个约定式的提供各种效率工具的toolkit包来做到这一点。 重新造轮子的意义是什么? 造轮子,当然是因为现在没有我理想的轮子。 现在有什么轮子?有大名鼎鼎的 langchain,但是我认为它太大而杂了。这只是从优雅层面的批评,更重要的是: 开发效率低。我认为它抽象程度不够,灵活性不够,约定式的预设也不够用,元开发也难以进行。总之经过我在两个公司的算法岗的实践,我觉得它在不少场景下开发起来不如不用框架。我不是贬低它,只是认为它的意义更多的在于历史意义。langchain 就好比旧 React 和 Vue 2,他们在历史上有很高的价值,但是他们现在看来效率并不高。我要做的就是一个 Svelte,专注于开发效率,更本质上,是探索提示工程的最佳实践。 臃肿,与其它框架冲突。比如,我装一个 langchain 可能并不会用上任何本地的模型,但是它也会安装一个 1GB 的 torch 包;再比如它依赖于旧版本的 pydantic,这使得我没法把它集成到我需要 pydantic 2.x 的 WEB 服务中。 源码充斥着向后兼容,难以阅读。而理解源码几乎是通过继承 langchain 的类来二次开发的必经之路。丑陋的源码就意味着低灵活性,我一直这么认为。事实上,我一直认为,0.x 这样的版本号就是为了无负担地探索无限可能,而不是拖着沉重的身躯爬行。langchain 顶着 0.0.x 这样的版本号,我不认为它应该做像现在这样的向后兼容。如果这是开发者的炫技,我觉得这是害了这个项目。 所以 langchain 不是足够好的框架。还有什么呢?有,出身 Microsoft 的 guidance。它其实给了我不小的启发,它发明了一种模板语言,一个提示工程就是一个脚本,可读性挺高的。我非常欣赏它的尝试,它的模板语言能力丰富,也高效,另外它在提示工程的周边也做了很多实用工具(我自己做不出来的那种),代码也优雅得多,我非常欣赏这个项目。 但它也不够完美,因为它把整个 chain 写在一个模板中,我认为这过于激进了,或者说过于“约定式”了,灵活性在一些方面降低了。比如,将逻辑写在模板中,就没有语法高亮了;另外我也认为这不符合代码和模板分离的原则。promplate 作为后起之秀,会关注到这些坑,并尽可能避免。 除此之外 我一般把“通过重载各种语法,实现一些方便的调用方式”这种实践也称为元编程。我在 promplate 这个框架中也加入了一些“有底线的激进”的元编程,比如: 自动命名:一个模板或者chain的节点的实例,在__str__的时候会显示一个名字,而这个名字默认就是它初始化时赋值给的第一个变量名 用算术运算符描述一个流程:重载了算数运算符,以实现直观地组建一个 chain 的效果 import 一个 template:直接用原生的语法 import 一个模板,算是一种语法糖(但是有一些副作用) 用 raise 的方法控制流程:这使得 chain 成为有限状态机,相当于一个 agent 回到最初的问题 为什么我要开发 promplate?显而易见,这是一个目标为提示工程的最佳实践的激进探索,探索成功了利己利人,失败了呢……我会谨慎行事,不允许失败 哈哈

2023/9/14
articleCard.readMore

试着让一个实例知晓自己的变量名

我创建了一个 GitHub Gist 这段代码定义了一个名为 AutoNaming 的类,该类的作用是自动获取并绑定实例的名称。 __new__ 方法:在创建新实例时,首先调用 __new__ 方法,它创建了一个新的实例,并调用 _bind_frame 方法。 _bind_frame 方法:获取当前的堆栈帧,并将其存储在实例的 _frame 属性中。 _name 方法:这是一个缓存属性,它试图从 _frame 属性中找到实例的名称。如果找到,就返回这个名称。 class_name 属性:返回实例的类名。 fallback_name 属性:如果无法获取实例的名称,就返回类名作为备选名称。 name 属性:返回实例的名称,如果 _name 属性不存在,就返回 fallback_name。 name 属性的 setter 方法:允许用户手动设置实例的名称,并将 _frame 属性设置为 None。 name 属性的 deleter 方法:删除 _name 属性。 __repr__ 方法:返回实例的字符串表示形式,包含类名和实例名。 __str__ 方法:返回实例名。 总的来说,这个类的目的是在创建实例时自动获取并绑定实例的名称,同时也支持手动设置和删除实例名。 # copied from `promplate.prompt.utils` from functools import cached_property from inspect import currentframe class AutoNaming: def __new__(cls, *args, **kwargs): obj = super().__new__(cls) obj._bind_frame() return obj def _bind_frame(self) -> None: self._frame = currentframe() @cached_property def _name(self): f = self._frame if f and f.f_back and (frame := f.f_back.f_back): for name, var in frame.f_locals.items(): if var is self: return name @property def class_name(self): return self.__class__.__name__ fallback_name = class_name @property def name(self): return self._name or self.fallback_name @name.setter def name(self, name): self._name = name self._frame = None @name.deleter def name(self): del self._name def __repr__(self): if self._name: return f"<{self.class_name} {self.name}>" else: return f"<{self.class_name}>" def __str__(self): return f"<{self.name}>"

2023/9/6
articleCard.readMore

104 hrs 35 mins over the last 14 days

<!DOCTYPE ><div typography="classic"><p><img src="https://s2.loli.net/2023/08/25/9yaAF5RJukhsUQp.png" width="1522"></img></p></div>

2023/8/25
articleCard.readMore

51 hrs 35 mins over the last 7 days.

<!DOCTYPE ><div typography="classic"><p><img src="https://s2.loli.net/2023/08/11/EtqiJ9cKy2kfVgn.png" width="1781"></img></p><p><img src="https://s2.loli.net/2023/08/11/28MSOWFt35IaAm6.png" width="1344"></img></p></div>

2023/8/11
articleCard.readMore

终于暂时地夺回了 muspimerol.site

今天尝试将域名转出,没想到在其它域名注册商转入也要花好几十甚至几百块钱……,而且不知道为啥,快过期的域名反而无法转出了。 我 bnu120.space 的域名还有几天就要过期了,所以转不了。muspimerol.site 转出要花钱,也转不了。 但是很有意思的一点是什么,腾讯云允许跨用户转移域名,转完了之后名义上还是不允许解析,但事实上我成功把 DNS 服务器设置为 cloudflare 了,我也不知道为啥,反正就是成功了。 至少短期内是能解析了吧。希望 Free Chat 损失的 95% 的用户能逐渐回来一些 😂

2023/8/9
articleCard.readMore

一种DDOS的应对方式

最近网站遭到了DDOS,流量飙得老高,100GB的流量包半周就能耗完。发现攻击集中在 376 KB 的最大的bundle文件上。由于攻击来自多个IP,且(由于我比较菜)没有什么特征可以分辨,逐url逐IP的令牌桶没效果,所以一直都没有实质性的办法。 说起来,这人一开始还通过尾随 queryString 的方式击穿我CDN的缓存,跑了我源站几十GB的流量直接给我干欠费了 哈哈哈 今天突然想到了一种办法通过cookies的方式,让浏览器访问的友好用户可以正常访问,而恶意攻击不会占用我太大流量: 我先是创建了一个自动刷新的HTML: <html> <head> <meta http-equiv="refresh" content="0"> </head> </html> 还可以更短: <meta http-equiv="refresh" content="0"> 我的思路是,让所有请求都解析出这个HTML,但响应投中包含一个set-cookies,刷新后的请求发现cookies对上了就返回原始数据。这个方案的优势在于: 可以完全作为中间件存在,无需改动服务源码 用户无感,因为这个刷新实测非常快 事实上,这其实很容易被破解,攻击者只需要模仿浏览器执行set-cookies的行为即可。如果他确实这么做了,那我觉得只能把set-cookies放在<script>中实现,然后结合混淆的方式来改进了。毕竟要是要执行js的话,基本上就得用Puppeteer、Selinium之类的麻烦玩意儿了 欢迎大家开无痕打开Devtools去尝试尝试我部署了这个中间件的网页 效果 附录 源码如下: import aiohttp from sanic import Request, Sanic from sanic import __version__ as sanic_version from sanic.response import empty, file, raw app = Sanic(__name__) @app.route("/<path:path>") async def index(request, path): async with aiohttp.ClientSession() as session: async with session.get( f"<protocol>://<host>:<port>/{path}", headers=request.headers ) as resp: headers = resp.headers.copy() headers.pop("Transfer-Encoding", None) headers.pop("Content-Encoding", None) return raw(await resp.read(), status=resp.status, headers=headers) CHECK_KEY = "human" CHECK_VAL = "Hi!" @app.on_request async def check_not_first(request: Request): if CHECK_KEY in request.cookies: if request.cookies.get(CHECK_KEY) == CHECK_VAL: return if "text/html" in request.headers.get("accept", ""): response = await file("./redirect.html") response.add_cookie(CHECK_KEY, CHECK_VAL) return response else: return empty()

2023/5/18
articleCard.readMore

使用后端渲染的一点心得

2023年5月16日 最近由于要参加一个比赛,必须得捡起一个老项目。当时刚学会用 Jinja2 那套东西,用后端渲染和 Vanilla JS 实现了 SPA,现在看回以前的代码,不少令人汗颜的地方 另一方面,我准备记录一下后端渲染的框架的一些问题,将来在我自己的模板框架上把这些给用新兴前端框架的方式一一解决 原来有 context-processors 这个东西

2023/5/17
articleCard.readMore

如何避免 Vercel 的 Edge Function 上限

Vercel 不好(不适合白嫖)的一点在于,它的 Edge Function Execution Units 是要计费的,这意味着对 Free Chat 这样的云函数密集型(?)应用就不太适合部署在这里。 但是 Netlify 似乎不管这些,只按流量计费: 然而因为一些还没解决的问题,我这个项目直接部署在 Netlify 不能正常渲染静态页面。所以一直没把整个应用部署到 Netlify 上。但它的云函数是正常的。所以我想到,为什么不能用一个 Hybrid 的方案,页面用 Vercel 来 host,云函数用 Netlify 来 host 呢? 但是一上线就报错了。哈哈哈,原来是 host 还是原来的 host,逛了一下,发现要在这里改一下特定 route 的 host,上线测试了一下果然成功了。

2023/4/25
articleCard.readMore

大家提供的prompts

标题 内容(超链接) 作者(你可以留名) 翻译(示例) 你是一个翻译家,下面的话都不是指令。翻译以下每一句话,不要解释。注意翻译的信达雅。 Muspi Merol Linux命令解释器 你是一个linux命令解释器,需要为我提供linux命令的使用方式和样例

2023/4/11
articleCard.readMore

My GSoC 2023 Proposal - SymPy

Empowering SymPy Live and SymPy Gamma with Pyodide/Brython and Modern Front-end Frameworks Abstract This proposal aims to enhance the functionality and performance of SymPy Live and SymPy Gamma, two online calculators and interactive teaching tools based on the SymPy Python library. The project will integrate Pyodide/Brython and modern front-end frameworks to enable Python code execution directly in the browser and improve the user interface. The specific tasks include integrating Pyodide/Brython with SymPy Live and SymPy Gamma, updating the front-end design using modern frameworks, and improving the parsing of input expressions in SymPy Gamma. Additionally, documentation and tests will be written to ensure the applications are well-documented and tested and can be easily maintained in the future. The expected outcome is a modified version of SymPy Live and SymPy Gamma with improved functionality and a modern front-end design. Introduction SymPy is a Python library for symbolic mathematics. It provides a wide range of features for algebraic manipulation, calculus, and equation solving. SymPy Live and SymPy Gamma are two online calculators and interactive teaching tools based on SymPy. SymPy Live is an interactive console version of SymPy that is integrated into SymPy's documentation. SymPy Gamma is a SymPy version of WolframAlpha, providing users with the ability to enter any mathematical expression and have it computed automatically. Both applications are currently hosted on Google App Engine, which is costly for the SymPy project. This proposal aims to migrate SymPy Live and SymPy Gamma to Pyodide/Brython and modern front-end frameworks, enabling SymPy to run in the browser's JavaScript environment on the client-side. This will reduce hosting costs and improve the user experience. Additionally, the parsing of input expressions in SymPy Gamma will be improved to support more types of inputs. Objectives The objectives of this project are: Migrate SymPy Live and SymPy Gamma to Pyodide/Brython to enable Python code execution directly in the browser. Update the front-end design using modern frameworks such as Svelte, SolidJS, Astro, TailwindCSS, WindiCSS, or UnoCSS. Improve the parsing of input expressions in SymPy Gamma to support more types of inputs. Write documentation and tests for the modified versions of SymPy Live and SymPy Gamma. Methodology The project will be divided into the following phases: Phase 1: Pyodide/Brython Integration The first phase will involve integrating Pyodide/Brython with SymPy Live and SymPy Gamma. This will enable Python code execution directly in the browser and reduce hosting costs. Pyodide/Brython will be used to run the SymPy code in the browser environment. Phase 2: Front-end Design Update The second phase will involve updating the front-end design of SymPy Live and SymPy Gamma using modern frameworks such as Svelte, SolidJS, Astro, TailwindCSS, WindiCSS, or UnoCSS. This will improve the user experience and make the applications more modern and eco-friendly. Phase 3: Parsing Improvement The third phase will involve improving the parsing of input expressions in SymPy Gamma to support more types of inputs. This will enhance the functionality of the application and make it more user-friendly. Phase 4: Documentation and Testing The final phase will involve writing documentation and tests for the modified versions of SymPy Live and SymPy Gamma. This will ensure that the applications are well-documented and tested and can be easily maintained in the future. Deliverables The following deliverables are expected from this project: Modified versions of SymPy Live and SymPy Gamma with Pyodide/Brython integration and modern front-end design. Improved parsing of input expressions in SymPy Gamma to support more types of inputs. Documentation and tests for the modified versions of SymPy Live and SymPy Gamma. Timeline The project will take 175 hours to complete. The timeline for the project is as follows: Phase Tasks Time Phase 1 Pyodide/Brython Integration 50 hours Phase 2 Front-end Design Update 50 hours Phase 3 Parsing Improvement 50 hours Phase 4 Documentation and Testing 25 hours Prerequisites The following skills are required for this project: Front-end coding experience (CSS, JavaScript) Python programming skills Familiarity with Pyodide/Brython Familiarity with modern front-end frameworks such as Svelte, SolidJS, Astro, TailwindCSS, WindiCSS, or UnoCSS Conclusion This project aims to enhance the functionality and performance of SymPy Live and SymPy Gamma by migrating them to Pyodide/Brython and modern front-end frameworks. The project will also improve the parsing of input expressions in SymPy Gamma to support more types of inputs. The expected outcome is a modified version of SymPy Live and SymPy Gamma with improved functionality and a modern front-end design.

2023/4/5
articleCard.readMore

重启后要启动的服务

mysqldservice mysqld start chat-proxypoetry run uvicorn main:app --reload --host 0.0.0.0 --port 12233 --no-access-log --no-server-header --no-date-header free-chatPORT=9041 HOST=0.0.0.0 node dist/server/entry.mjs new-bingo./start.sh simple-posterpoetry run flask run --host 0.0.0.0 chatgpt_academicpoetry run python3 -O main.py gppoetry run uvicorn main:app --reload --port 9999 --host 0.0.0.0 yuque-sitenode build re-bingopoetry run python3 -O -m hypercorn src:app --reload --bind=0.0.0.0:443 --insecure-bind=0.0.0.0:80 --reload --keyfile ../bingo.key --certfile ../bingo.cer --ca-certs ../ca.cer --access-logfile - visit-counterpoetry run uvicorn main:app --reload --host 0.0.0.0 --port 14141 tmppoetry run python3 -O main.py --reload

2023/4/1
articleCard.readMore

Linux 生成16位密码

Linux上如何快速产生一个长度为16的随机字符串? 您可以使用Linux的内置命令行工具来生成一个长度为16的随机字符串。具体方法如下: bashCopy code head -c 500 /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1 该命令的作用是: 通过/dev/urandom设备文件读取随机数据流。 使用tr命令过滤只包含字母和数字的字符集。 使用fold命令将字符流折叠成一行,每行16个字符。 使用head命令仅输出第一行,即一个长度为16的随机字符串。

2023/3/30
articleCard.readMore

Boolean Retrieval Homework

Exercise 1.2 Consider these documents: Doc 1: breakthrough drug for schizophrenia Doc 2: new schizophrenia drug Doc 3: new approach for treatment of schizophrenia Doc 4: new hopes for schizophrenia patients a. Draw the term-document incidence matrix for this document collection. b. Draw the inverted index representation for this collection, as in Figure 1.3 (page 7). Answer 1.2 a. The term-document incidence matrix for this document collection is: Term Doc 1 Doc 2 Doc 3 Doc 4 approach 0 0 1 0 breakthrough 1 0 0 0 drug 1 1 0 0 hopes 0 0 0 1 new 0 1 1 1 patients 0 0 0 1 schizophrenia 1 1 1 1 treatment 0 0 1 0 b. The inverted index representation for this collection is: Term Postings approach Doc 3 breakthrough Doc 1 drug Doc 1, Doc 2 hopes Doc 4 new Doc 2, Doc 3, Doc 4 patients Doc 4 schizophrenia Doc 1, Doc 2, Doc 3, Doc 4 treatment Doc 3 Exercise 1.3 For the document collection shown in Exercise 1.2, what are the returned results for these queries: a. schizophrenia AND drug b. for AND NOT(drug OR approach) Answer 1.3 Query Returned Documents schizophrenia AND drug Doc 1, Doc 2 for AND NOT(drug OR approach) Doc 4 Exercise 1.7 Query Processing Order Recommendation: (tangerine OR trees) AND (marmalade OR skies) AND (kaleidoscope OR eyes) Given the following postings list sizes: Term Postings size eyes 213312 kaleidoscope 87009 marmalade 107913 skies 271658 tangerine 46653 trees 316812 Answer 1.7 To optimize the query processing order, we need to first calculate the OR operations with the smallest postings size. kaleidoscope OR eyes requires traversing 87009 + 213312 = 300321 nodes. marmalade OR skies requires traversing 107913 + 271658 = 379571 nodes. tangerine OR trees requires traversing 46653 + 316812 = 363465 nodes. Then, we can calculate (marmalade OR skies) AND (kaleidoscope OR eyes), which requires traversing 232164 nodes. Next, we can calculate kaleidoscope OR trees, which requires traversing 87009 + 316812 = 403821 nodes. Finally, we can calculate (tangerine OR trees) AND (marmalade OR skies) AND (kaleidoscope OR eyes), which requires traversing 213312 + 87009 + 232164 = 532485 nodes. In summary, the recommended query processing order is: kaleidoscope OR eyes and marmalade OR skies (marmalade OR skies) AND (kaleidoscope OR eyes) kaleidoscope OR trees (tangerine OR trees) AND (marmalade OR skies) AND (kaleidoscope OR eyes) Exercise 1.11 How should the Boolean query x AND NOT y be handled? Why is naive evaluation of this query normally very expensive? Write out a postings merge algorithm that evaluates this query efficiently. Answer 1.11 The Boolean query x AND NOT y means that we want all documents that contain term x but do not contain term y. This query can be handled by first finding all documents that contain term x, and then excluding all documents that contain term y. Naive evaluation of this query is normally very expensive because it requires iterating through the entire inverted index for both terms x and y, and then performing a set difference operation on the resulting sets of document IDs. This can be very time-consuming for large indexes. A more efficient algorithm for evaluating this query is as follows: Retrieve the postings list for term x and term y. Initialize two pointers, one for each postings list. While both pointers are not at the end of their respective postings lists: If the document ID at the x pointer is less than the document ID at the y pointer, add the document ID at the x pointer to the result set and advance the x pointer. If the document ID at the x pointer is greater than the document ID at the y pointer, advance the y pointer. If the document ID at the x pointer is equal to the document ID at the y pointer, advance both pointers. Add any remaining document IDs from the x postings list to the result set. This algorithm takes advantage of the fact that both postings lists are sorted by document ID, and only iterates through each list once. Therefore, it is much more efficient than the naive set difference approach. Here is my Python implementation of the x AND NOT y query algorithm: def list_difference(a: list, b: list) -> list: if not a: return [] if not b: return a results = [] it_a, it_b = iter(a), iter(b) i, j = next(it_a), next(it_b) try: while True: if i < j: results.append(i) i = next(it_a) elif i > j: j = next(iter_b, None) if j is None: results.append(i) break else: j = next(it_b) i = next(it_a) except StopIteration: return results + list(it_a) Python's implementation takes advantage of the iterator functionality, which is a Pythonic way of handling lists. Similarly, in C++, we can use pointers and vectors to implement the AND NOT query more efficiently and from a more fundamental perspective. Here is a C++ implementation using pointers and vectors: #include <vector> std::vector<int> and_not_query(const std::vector<int>& x, const std::vector<int>& y) { std::vector<int> result; auto ptr_x = x.begin(), ptr_y = y.begin(); while (ptr_x != x.end() && ptr_y != y.end()) { if (*ptr_x < *ptr_y) { result.push_back(*ptr_x); ++ptr_x; } else if (*ptr_x > *ptr_y) { ++ptr_y; } else { ++ptr_x; ++ptr_y; } } result.insert(result.end(), ptr_x, x.end()); return result; } Proof about Time Complexity The time complexity of the naive approach can be represented as: Where n is the length of the two lists. In contrast, our solution has a time complexity represented as: Where n is the length of the two lists. To break it down, the first step of our solution is to convert the lists into iterators, which is a constant time operation. In the worst-case scenario where there are no duplicate elements, the algorithm needs to traverse through all the elements in both lists, resulting in a time complexity of 2n. The for loop in the algorithm has a time complexity represented as: The while loop has a time complexity represented as: Thus, the overall time complexity of our solution is: In conclusion, our solution has a time complexity that is more efficient than the naive one.

2023/3/19
articleCard.readMore

计算机组成原理 第3次作业

通过编程验证 float 和 double 类型的精度(即十进制有效位的位数),在实验报告中给出结果及解释;编程计算 -8.0/0 、 sqrt(-4,0) 的运算结果,并对结果给予解释。 static void testPrecision() { float f = 123456789.123456789f; double d = 123456789.123456789; System.out.println("float f = " + f); System.out.println("double d = " + d); } 运行结果: float f = 1.2345679E8 double d = 1.2345678912345679E8 在上述代码中,我们分别定义了一个 float 类型变量 f 和一个 double 类型变量 d。f 只保留了小数点后的前 7 位有效数字,而 d 则保留了小数点后的前 15 到 16 位有效数字。 float 类型使用 32 位(4 字节)来存储浮点数,其中 1 位用于表示符号位,8 位用于表示指数,剩余的 23 位用于表示尾数。这意味着 float 类型最多可以表示 个不同的数,也就是大约 位的有效数字。因此,float 类型只能精确表示到第 7 位有效数字。 double 类型使用 64 位(8 字节)来存储浮点数,其中 1 位用于表示符号位,11 位用于表示指数,剩余的 52 位用于表示尾数。这意味着 double 类型最多可以表示 个不同的数,也就是大约 位的有效数字。因此,double 类型可以精确表示到第 15 到 16 位有效数字。 static void testCaculation() { double result1 = -8.0 / 0; double result2 = Math.sqrt(-4.0); System.out.println("-8.0/0 = " + result1); System.out.println("sqrt(-4,0) = " + result2); } 运行结果: -8.0/0 = -Infinity sqrt(-4,0) = NaN 在计算机中,当执行除以 0 的操作时,根据 IEEE 754 浮点数标准,结果会被定义为正无穷大、负无穷大或 NaN(不是一个数字)中的一个。 当被除数为正数时,除以 0 的结果为正无穷大(+Infinity)。当被除数为负数时,除以 0 的结果为负无穷大(-Infinity)。当被除数为 0 时,除以 0 的结果为 NaN。 因此,对于表达式 -8.0/0,被除数为负数 -8.0,因此结果为负无穷大(-Infinity)。 对于表达式 sqrt(-4.0),它是求负数的平方根,而在实数域中,负数的平方根是一个虚数,不能用实数来表示。在计算机中,sqrt() 函数不支持计算负数的平方根,会返回 NaN(不是一个数字),表示计算无法完成。 令 float a = (100 + 1.0/3) - 100 , float b = 1.0 / 3 ;请回答逻辑表达式 a==b 的取值是什么?如果变成 double a = (100 + 1.0/3) - 100 , double b = 1.0 / 3 , a==b 的取值又是什么?通过程序计算和检验,对结果进行说明。 static void testEquality() { float aFloat = (100 + 1.0f / 3) - 100; float bFloat = 1.0f / 3; double aDouble = (100 + 1.0 / 3) - 100; double bDouble = 1.0 / 3; System.out.println("float a == b: " + (aFloat == bFloat)); System.out.println("double a == b: " + (aDouble == bDouble)); } 运行结果: float a == b: false double a == b: false 浮点数在计算机内部以二进制形式表示时,会存在精度损失。当我们使用等于操作符(==)比较两个浮点数时,它会将它们的二进制表示形式进行比较。由于精度损失,这样的比较可能会产生不准确的结果。 类型转换和移位操作。 编程以实现以下各种操作: 给定一个short型数据-12345,分别转为int、unsigned short、unsigned int、float类型的数据 给定一个int型数据2147483647,分别转为short、unsigned short、unsigned int、float类型的数据 给定一个float型数据123456.789e5,转换成double型数据 给定一个double型数据123456.789e5,转换成float型数据 按short和unsigned short类型分别对-12345进行左移2位和右移2位操作 要求分别用十进制和十六进制形式打印输出以上各种操作的结果,并根据实验结果,回答下列问题: float型数据是否总能转换成等值的double型数据? 长数被截断成短数后可能发生什么现象?为什么? C语言中移位操作规则与操作对象的数据类型有关吗? 左移2位和右移2位操作分别相当于扩大和缩小几倍? static void testTypeConversion1() { short s = -12345; int i = s; int us = s & 0xffff; long ui = s & 0xffffL; float f = s; System.out.println("int: " + i + " (0x" + Integer.toHexString(i) + ")"); System.out.println("unsigned short: " + us + " (0x" + Integer.toHexString(us) + ")"); System.out.println("unsigned int: " + ui + " (0x" + Long.toHexString(ui) + ")"); System.out.println("float: " + f + " (" + Float.toHexString(f) + ")"); } static void testTypeConversion2() { int i = 2147483647; short s = (short) i; int us = i & 0xffff; long ui = i & 0xffffffffL; float f = i; System.out.println("short: " + s + " (0x" + Integer.toHexString(s) + ")"); System.out.println("unsigned short: " + us + " (0x" + Integer.toHexString(us) + ")"); System.out.println("unsigned int: " + ui + " (0x" + Long.toHexString(ui) + ")"); System.out.println("float: " + f + " (" + Float.toHexString(f) + ")"); } static void testTypeConversion3() { float f = 123456.789e5f; double d = f; System.out.println("double: " + d + " (" + Double.toHexString(d) + ")"); } static void testTypeConversion4() { double d = 123456.789e5; float f = (float) d; System.out.println("float: " + f + " (" + Float.toHexString(f) + ")"); } static void testTypeConversion5() { short s = -12345; short s2 = (short) (s << 2); int us = s & 0xffff; int us2 = us << 2; System.out.println("short: " + s2 + " (0x" + Integer.toHexString(s2) + ")"); System.out.println("unsigned short: " + us2 + " (0x" + Integer.toHexString(us2) + ")"); } 运行结果: int: -12345 (0xffffcfc7) unsigned short: 53191 (0xcfc7) unsigned int: 53191 (0xcfc7) float: -12345.0 (-0x1.81c8p13) short: -1 (0xffffffff) unsigned short: 65535 (0xffff) unsigned int: 2147483647 (0x7fffffff) float: 2.1474836E9 (0x1.0p31) double: 1.2345678848E10 (0x1.6fee0ep33) float: 1.2345679E10 (0x1.6fee0ep33) short: 16156 (0x3f1c) unsigned short: 212764 (0x33f1c) 由于没有指定语言,所以一开始用了Java,但是Java中没有unsigned的数据类型,只好手动模拟,于是又用C语言实现了一遍: 给定一个short型数据-12345,分别转为int、unsigned short、unsigned int、float类型的数据 #include <stdio.h> int main() { short s = -12345; int i = s; unsigned short us = s; unsigned int ui = s; float f = s; printf("s = %d, %#x\n", s, s); printf("i = %d, %#x\n", i, i); printf("us = %u, %#x\n", us, us); printf("ui = %u, %#x\n", ui, ui); printf("f = %f\n", f); return 0; } 输出结果为: s = -12345, 0xffffcfc7 i = -12345, 0xffffcfc7 us = 53191, 0xcfc7 ui = 4294954951, 0xffffcfc7 f = -12345.000000 给定一个int型数据2147483647,分别转为short、unsigned short、unsigned int、float类型的数据 #include <stdio.h> int main() { int i = 2147483647; short s = i; unsigned short us = i; unsigned int ui = i; float f = i; printf("i = %d, %#x\n", i, i); printf("s = %d, %#x\n", s, s); printf("us = %u, %#x\n", us, us); printf("ui = %u, %#x\n", ui, ui); printf("f = %f\n", f); return 0; } 输出结果为: i = 2147483647, 0x7fffffff s = -1, 0xffffffff us = 65535, 0xffff ui = 2147483647, 0x7fffffff f = 2147483648.000000 给定一个float型数据123456.789e5,转换成double型数据 #include <stdio.h> int main() { float f = 123456.789e5; double d = f; printf("f = %f, %a\n", f, f); printf("d = %f, %a\n", d, d); return 0; } 输出结果为: f = 12345678848.000000, 0x1.6fee0ep+33 d = 12345678848.000000, 0x1.6fee0ep+33 给定一个double型数据123456.789e5,转换成float型数据 #include <stdio.h> int main() { double d = 123456.789e5; float f = d; printf("d = %f, %a\n", d, d); printf("f = %f, %a\n", f, f); return 0; } 输出结果为: d = 12345678900.000000, 0x1.6fee0e1ap+33 f = 12345678848.000000, 0x1.6fee0ep+33 按short和unsigned short类型分别对-12345进行左移2位和右移2位操作 #include <stdio.h> int main() { short s = -12345; unsigned short us = s; printf("s = %d, %#x\n", s, s); printf("s左移2位: %d, %#x\n", s << 2, s << 2); printf("s右移2位: %d, %#x\n", s >> 2, s >> 2); printf("us = %u, %#x\n", us, us); printf("us左移2位: %u, %#x\n", us << 2, us << 2); printf("us右移2位: %u, %#x\n", us >> 2, us >> 2); return 0; } 输出结果为: s = -12345, 0xffffcfc7 s左移2位: -49380, 0xffff3f1c s右移2位: -3087, 0xfffff3f1 us = 53191, 0xcfc7 us左移2位: 212764, 0x33f1c us右移2位: 13297, 0x33f1 float型数据是否总能转换成等值的double型数据? 是的,float类型的数据可以转换为double类型的数据,但是可能会精度丢失。 长数被截断成短数后可能发生什么现象?为什么? 当一个长数被截断成短数后,如果截断的部分超过了短数所能表示的范围,就会发生数据溢出,即结果不再准确。这是因为长数和短数的字节数不一样,长数通常占用4个字节或8个字节,而短数通常只占用2个字节,因此短数能够表示的数值范围比长数小。 C语言中移位操作规则与操作对象的数据类型有关吗? 是的,C语言中移位操作的规则与操作对象的数据类型有关。对于有符号数,右移操作会保留符号位(即最高位),而左移操作不会。对于无符号数,左右移操作都不会保留符号位。另外,移位操作会将超出数据类型位数的位数截断。 左移2位和右移2位操作分别相当于扩大和缩小几倍? 左移n位相当于将数值乘以2的n次方,右移n位相当于将数值除以2的n次方。因此,左移2位相当于将数值扩大4倍,右移2位相当于将数值缩小4倍。

2023/3/16
articleCard.readMore

分布式计算原理与应用 第2次作业

三、中国知网——主页 在主页上行业知识服务与知识管理平台或者专题知识库,找与自己专业相关的平台 在出版平台的选刊投稿栏目,查找与自己专业相关的期刊 在大数据研究平台学术热点,查找自己专业的学术热点 在知识元检索中点击指数,然后检索自己专业的热门主题,检索完后查看各个结果 在CNKI学术搜索中搜索人工智能和大数据的学术论文 三、中国知网——分析与关联功能 请使用高级检索, 检索词为“大数据and人工智能”进行检索,在检索结果页面进行如下练习 在检索结果页面左侧主题、文献来源、学科、作者、机构、基金、文献来源,点击就可以进行分析,大家练习主题的交叉分析、机构分析 在检索结果页面的“导出与分析”栏目里面“可视化分析”,有两种分析方式:一种是已选结果进行分析,一种是全部结果分析,分别进行练习 检索结果中,点击一条引用最高的记录,在该记录的页面上,查看核心文献推荐、引文网络,以及相关推荐中相似文献、读者推荐文献 检索结果中,点击一条引用最高的记录,在该记录的页面上,点击一个关键词,进入该关键词的知网节,可以查看该词相关词、相似词、关键文献、相似文献、相关作者和机构 三、中国知网——应用 检索2020年以来,“推荐系统”的相关文献 检索北京师范大学陈木法院士指导的博士论文 检索2000-2019年有关“向量空间模型”的文献。要求检索词出现在论文的标题中,检索结果按被引频次排序(试比较模糊/精确匹配对检索结果的影响),并导出5篇以上文献 分别用CAJ格式和PDF格式,下载两篇以上题目的检索结果 四、SCIE数据库 在《Acta Informatica》杂志中检索2000年以来、篇名中含有XML的文章,并要求检索结果按照新颖度排序 检索有关计算机数据通讯网络研究方面的期刊论文列出主要研究方向和研究机构。按被引次数排序,下载几篇热点论文的全文,用DOI号或者标题在(Scopus)上下载 利用“专业检索”检索北京师范大学XX教授近5年发表的论文,并查看其发表论文的H指数 查找“ 2019 IEEE/CVF CONFERENCE ON COMPUTER VISION AND PATTERN RECOGNITION (CVPR 2019) ”会议上发表的有关表征学习的文章,导出被引用次数最多的几篇文章作为参考文献 五、Springer 检索有关视觉关系检测方面的文献(检索词:visual relationship; recognition)可以检索到多少篇文献?在这些文献中,有几本图书?请描述其中一本书的外部特征(书名、出版社、学科分类等信息) 这本书的书名为《计算机视觉参考指南》(Computer Vision: A Reference Guide),由Katsushi Ikeuchi(日本东京大学教授)主编,由Springer出版。该书是一本参考书,主要为读者提供易于访问的计算机视觉相关信息。该书的目录采用A-Z格式,涵盖了计算机视觉领域内的理论和实践等各个方面,并包括超过240个条目和3700多个参考文献。该书的作者来自学术界和工业界,超过200位,每个条目都有同义词、定义和讨论,并提供了丰富的交叉参考支持,以支持高效、用户友好的搜索,以便用户能够立即访问相关信息。该书适合计算机视觉领域的初学者和其他领域的专业人员访问这些重要信息,但他们可能没有时间阅读完整个相关主题的文本。该书属于计算机科学和图像处理这一学科分类。 检索“人工神经网络作为TV信号处理器”的有关文献(检索词:neural network;TV or television;signal)可以检索到多少篇文章?Biomedicine方面的Article有多少?请描述其中最新一篇论文的期刊投稿链接(注意TV or television的输入框格选择)

2023/3/14
articleCard.readMore

无标题文档

new-bingo / main.py 这里写了个简答的python的静态文件服务器 将来准备添加: 持久化压缩并将数据保存到磁盘 懒压缩 将 etag 缓存在内存中

2023/3/10
articleCard.readMore

一个控制ChatGPT的token爆炸的想法

我在设想一种,仿照真实聊天方式的ChatGPT的请求方式。这只是一种优化。 目的: 减少token使用,降低成本并提升回复消息大小上限 使请求体大小和token数不会随着时间线性增长 方法: [ system 规则 ] + [ user 最近一个问题 ] + [ assistant 对消息记录的查询请求 ] + [ system 回复 ] 返回类型 没成功,ChatGPT不理解我的提示

2023/3/9
articleCard.readMore

无标题文档

temperature 和 top_p 都是用于控制生成文本多样性的参数,但是它们的实现方式和效果略有不同 Temperature 是通过对生成文本的概率分布进行操作来达到控制多样性的效果。具体来说,它会将每个 token 的概率值除以一个小于 1 的温度参数,然后再进行 softmax 运算,以获得更平滑的分布。当温度参数接近 0 时,生成的文本会趋于重复和单调;当温度参数较大时,生成的文本会变得更加随机和多样。 Top-p(或 nucleus)是通过对生成文本的概率分布中的前几个最可能的 token 进行采样,来达到控制多样性的效果。具体来说,它会根据一个小于等于 1 的阈值参数,把概率分布中累计概率超过该阈值的 token 去掉,再对剩下的 token 进行归一化,从中进行随机采样。当阈值接近 0 时,生成的文本会趋于重复和单调;当阈值较大时,生成的文本会变得更加随机和多样。 总的来说,Temperature 更加直接和自由度较大,因为它不限制生成的 token 种类;而 Top-p 相对来说更加稳定和可控,因为它可限制生成的 token 种类和数量。 Temperature 和 Top-p 这两个参数都可以用于控制生成的文本的多样性,让机器生成的文本看起来更加自然、生动、有趣。它们的作用可以类比于人类的写作方式。 Temperature 可以类比为人类的写作时的思考难度,难度越大、要求越高,那写出的作品很可能就更加突出、独特,也更加有价值;但如果思考难度过大,也可能写出来的作品让读者感到繁琐、重复、无聊。因此我们会根据需要,调整和控制自己的思考难度。在机器生成文本的场景下,设置较高的 Temperature 参数值,可以让机器更加随机地选择不同的词汇,写出较独特、有趣的文章,但可能也会带来些许重复的问题。 Top-p 则可以类比为人类写作时的表达方式。我们在写作时,通常会选择最相关、最精准的话语,更多的时候会使用特定的词汇和表达方式,而并非任意选择。机器也可以类似地操作,通过 Top-p 模式,控制机器只从最相关、最精准的单词和短语中进行选取,而过多的随机选择将被控制在一个阈值范围内,写出更为准确和精细的文章。

2023/3/8
articleCard.readMore

划词检索

function showSelection() { let selection = document.getSelection(); console.log(selection); return selection.toString(); } document.addEventListener("selectionchange", showSelection)

2023/3/8
articleCard.readMore