<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tech on 猜猜是谁的博客</title><link>https://caifengze.com/zh-cn/categories/tech/</link><description>Recent content in Tech on 猜猜是谁的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 19 Jun 2026 14:28:37 -0400</lastBuildDate><atom:link href="https://caifengze.com/zh-cn/categories/tech/index.xml" rel="self" type="application/rss+xml"/><item><title>CodexGo 回忆录：在 Auto Approve 出现前夜</title><link>https://caifengze.com/zh-cn/blog/2026/codexgo-memoir-before-auto-approve/</link><pubDate>Fri, 19 Jun 2026 14:28:37 -0400</pubDate><guid>https://caifengze.com/zh-cn/blog/2026/codexgo-memoir-before-auto-approve/</guid><description>&lt;h2 id="写在前面">写在前面
&lt;/h2>&lt;p>没事看了一下自己的 blog，又是大几个月没更新了，其实脑子里一直都有很多idea，不过陪伴，工作，各种事情充斥着生活，也没多少时间记记想记录的事情。&lt;/p>
&lt;p>一直都觉得&lt;strong>反思&lt;/strong>很重要，不过我好像也没常反思。把标题取做回忆录，既是给这个项目“入坟”的哀悼，也是一个让自己多反思的提点。&lt;/p>
&lt;p>我觉得自己当时应该是有点 &lt;a class="link" href="https://en.wikipedia.org/wiki/Fear_of_missing_out" target="_blank" rel="noopener"
>Fomo&lt;/a> 的。&lt;/p>
&lt;p>codex 的 desktop 横空出世，让我非常的开心，不用再每天盯着“编辑器”在那看代码了。说实在的，按照当时我用 ai 的开发习惯，最集中的注意力其实不是代码本身，而是转移到了项目架构，文件结构，代码规范，测试上。而 cc 和 codex 本质上就是 这种注意力转移的更显化的产品。&lt;/p>
&lt;p>在 cursor 和 codex plugin in VSCode 的时代，AI 写出来的代码，我总会点过去看看，到底长什么样，但我其实也不是一个coding freshman，觉得自己在学习，但也挑不出什么 AI 的毛病，只能反复问：你可以再检查一下自己的代码吗，看看哪里还有问题。一直问到，ai 自己也觉得没有什么问题，然后开始测试的循环了。&lt;/p>
&lt;p>我记得还问过 siwei 老师，到底怎么让 AI 的代码写的好呢？记得当时 siwei 老师应该已经开始大规模的 AI coding 造出了 nowledge-mem了，并且整体的完成度非常高，和我能做出来的玩意儿是在是差距真有点大哈哈哈。 siwei 老师回答大概的意思是，多验证，多测试，好像很简单，但其实这一套范式至今，也是 vibe coding 的非常合理化的手段。&lt;/p>
&lt;p>像前面说的 vibe coding 本质上是把注意力转移到架构上了，你变成指挥的人了，那在你手下干活的 coding agent，本质上也是一个会coding的“人”，你要保证他产出的东西有用，那应该用公司生产的那一套范式，让其他人（agent）去验证开发的对还是不对。&lt;/p>
&lt;p>至此，我觉得，测试，运维其实在这个时代的地位是上升了许多的。&lt;/p>
&lt;p>我想，这些话作为这篇文章的开头语，似乎又有点多了，可能我需要另有一篇来介绍自己对 AI coding 的理解吧。&lt;/p>
&lt;h2 id="正文">正文
&lt;/h2>&lt;h3 id="缘起">缘起
&lt;/h3>&lt;p>为什么我说自己 fomo 的，其实我是真想折腾一点好用的小工具出来，能够让自己有一个 star 基础的（貌似有点功利了）。&lt;/p>
&lt;p>所以我不断找啊找啊找需求。然后看到了 permission 这个问题上。这个问题确实很早就出现了: 在我还在用 vscode 的时候，其实并不放心，所有的 命令 ai 都能操作，但对 ask mode 又很厌烦，动不动2-3s一个 bash 就需要自己 approve，不然整个进度就会卡住。等我看到帖子里的很多人都在说这个问题的时候，codexgo 的 idea 就横空出世了——我需要一个工具就是能自动 approve 执行我觉得不重要的命令，也能为我拦下关键的操作。&lt;/p>
&lt;h3 id="借钩">借钩
&lt;/h3>&lt;p>当然，思路非常简单，就是在 codex 进行 &lt;code>PermissionRequest&lt;/code> 前利用 hook 加了一层判断，像这样&lt;/p>
&lt;pre tabindex="0">&lt;code>Codex PermissionRequest -&amp;gt; codexgo decide -&amp;gt; allow / deny / ask
&lt;/code>&lt;/pre>&lt;p>allow, deny, ask 区分很清楚，所有在 allow 范围里面的，都会直接批准;&lt;code>deny&lt;/code> 表示阻止；&lt;code>ask&lt;/code> 表示不做决定，交还给 Codex 原本的确认弹窗。&lt;/p>
&lt;h3 id="初成">初成
&lt;/h3>&lt;p>就单单这么说，显得项目就很简单了，那怎么能让用户直接就上手呢？&lt;/p>
&lt;p>就直接：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">codexgo init
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后你就可以在所有项目里都享受到 codex 的便利啦！&lt;/p>
&lt;p>当然，codexgo 还支持项目级别的配置，即:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">codexgo init --scope project
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>是不是像 &lt;code>git init&lt;/code> 一样简单！&lt;/p>
&lt;h3 id="分野">分野
&lt;/h3>&lt;p>当然，用户级别配置，config 会写在&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">~/.codex/config.toml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">~/.codex/hooks.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">~/.codexgo/policy.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果是 项目级 配置则是出现在&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&amp;lt;repo&amp;gt;/.codex/config.toml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;repo&amp;gt;/.codex/hooks.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;repo&amp;gt;/.codexgo/policy.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>并且项目级配置优先于用户级配置。&lt;/p>
&lt;h3 id="立规">立规
&lt;/h3>&lt;p>在 &lt;code>codex init&lt;/code> 后会自动生成一些基本配置，这个是按照正常的需求来的，比如，&lt;code>rm -rf /&lt;/code>、&lt;code>sudo&lt;/code> 等等命令肯定不用说，都是会被deny的；像 &lt;code>pwd&lt;/code>、&lt;code>rg&lt;/code> 、&lt;code>git diff&lt;/code> 等等没有风险的常用命令会被自动包括进去。默认的这些规则我们称其为一个 profile，这个profile 叫做 manual profile，其规则是&lt;/p>
&lt;pre tabindex="0">&lt;code>命中 allow 规则 -&amp;gt; 自动允许
命中 deny 规则 -&amp;gt; 直接拒绝
命中 ask 规则 -&amp;gt; 交给 Codex 弹窗确认
没有命中规则 -&amp;gt; 交给 Codex 弹窗确认
&lt;/code>&lt;/pre>&lt;p>当然，这会在遇到很多其他不常规的命令时，需要你手动添加一些规则才行，添加命令的方式如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">codexgo allow &amp;lt;command&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codexgo ask &amp;lt;command&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codexgo deny &amp;lt;command&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>具体例子就像&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">codexgo allow &lt;span class="s2">&amp;#34;git status&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codexgo deny &lt;span class="s2">&amp;#34;git reset --hard&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codexgo ask &lt;span class="s2">&amp;#34;git push&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当然，针对项目添加，只用像上面说的带上 &lt;code>--scope&lt;/code> 就好&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">codexgo allow --scope project &lt;span class="s2">&amp;#34;npm run lint&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codexgo ask --scope project &lt;span class="s2">&amp;#34;git push&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codexgo deny --scope project &lt;span class="s2">&amp;#34;rm -rf&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="辨形">辨形
&lt;/h3>&lt;p>不过给的例子都像是非常精准匹配的，但事实上，我们还可以指定匹配方式，例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">codexgo allow --scope project --match exact &lt;span class="s2">&amp;#34;npm test&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codexgo deny --scope project --match contains &lt;span class="s2">&amp;#34;curl | sh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中有三种匹配模式：&lt;/p>
&lt;pre tabindex="0">&lt;code>exact 完全匹配
prefix 前缀匹配，默认
contains 包含匹配
&lt;/code>&lt;/pre>&lt;p>这样，就可以在命令上更精细化控制了！&lt;/p>
&lt;h3 id="放行">放行
&lt;/h3>&lt;p>以上，已经是一个比较完善的产品了，但我还是懒，我只想要我明确禁止的命令不做，其他全部 pass 好啦！&lt;/p>
&lt;p>那这个时候就有一个 &lt;code>go profile&lt;/code>。&lt;/p>
&lt;p>默认 manual 模式下，只有命中明确规则的命令才会自动 allow / deny；其它命令都会回到 Codex 弹窗确认。&lt;code>go profile&lt;/code> 则是我只关注需要 deny 和 ask 的 命令，而剩余的命令全部都可以 自动放行，也就是如下逻辑：&lt;/p>
&lt;pre tabindex="0">&lt;code>go profile:
1. 先看 project / user 显式规则里的 deny / ask
2. 再看 go profile 风险规则里的 deny / ask
3. 再看 built-in defaults
4. 如果是复杂 shell / 重定向 / 下载写文件 / 命令替换等 -&amp;gt; ask
5. 如果还没命中，并且不是复杂命令 -&amp;gt; allow
&lt;/code>&lt;/pre>&lt;p>那其实这个产品，就更加便捷了，只用&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">codexgo go --scope project
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>就可以开始用啦！defaults 里面已经把大多数日常危险的命令都 deny 或者 ask 了，剩下的，就是无比丝滑！&lt;/p>
&lt;h3 id="潮来">潮来
&lt;/h3>&lt;p>刚做出来的时候，我自己用的也非常开心，不过没两天，你知道，codex 官方就提供了一个 anto approve for me 的选项，按调研过来看，这是一个小模型的实现。虽然规则不像我们这样严苛，但是解决了大部分问题，甚至逻辑上更加丝滑，不用用户再自己控制一些规则了。&lt;/p>
&lt;p>我自己也是，在用过自己做出来的产品一周后，发现，已经可以被 codex 的这个功能基本完全（不完全是因为我觉得还会有些命令，有用户）替代了。当时应该还是很感叹的：刚出世的小东西，一下子就没了用武之地。&lt;/p>
&lt;h3 id="余响">余响
&lt;/h3>&lt;p>虽然现在来看，觉得自己做的还挺有意义的，只不过在更优秀的产品面前，被淘汰也是很自然的事情。时代洪流裹挟着大家一起往前飘，努力划桨，为的是在前方插下一个属于自己的旗帜，但大海会慢慢涌过它；也许被淹没，也许被看到的人慷慨。&lt;/p>
&lt;p>如果你想看看这个旗帜短暂存在过的样子：&lt;a class="link" href="https://github.com/Fengzdadi/codexgo" target="_blank" rel="noopener"
>Fengzdadi/codexgo&lt;/a>。&lt;/p></description></item><item><title>为什么 OpenClaw 消耗 Token 这么快？</title><link>https://caifengze.com/zh-cn/blog/2026/%E4%B8%BA%E4%BB%80%E4%B9%88-openclaw-%E6%B6%88%E8%80%97-token-%E8%BF%99%E4%B9%88%E5%BF%AB/</link><pubDate>Thu, 05 Feb 2026 21:00:00 -0500</pubDate><guid>https://caifengze.com/zh-cn/blog/2026/%E4%B8%BA%E4%BB%80%E4%B9%88-openclaw-%E6%B6%88%E8%80%97-token-%E8%BF%99%E4%B9%88%E5%BF%AB/</guid><description>&lt;img src="https://caifengze.com/zh-cn/blog/2026/%E4%B8%BA%E4%BB%80%E4%B9%88-openclaw-%E6%B6%88%E8%80%97-token-%E8%BF%99%E4%B9%88%E5%BF%AB/overview.png" alt="Featured image of post 为什么 OpenClaw 消耗 Token 这么快？" />&lt;h1 id="为什么-openclaw-消耗-token-这么快">为什么 OpenClaw 消耗 Token 这么快？
&lt;/h1>&lt;p>这篇文章的第一部分回答一个最基础、但也最关键的问题：&lt;/p>
&lt;h2 id="每次交互openclaw-到底向模型发送了什么">每次交互，OpenClaw 到底向模型发送了什么？
&lt;/h2>&lt;p>当我们说“上下文很大、token 消耗很快”，本质上是在说：每一次调用模型 API 时，OpenClaw 会把一段“完整输入”发送给模型，这段输入往往远超过用户的当前一句话。&lt;/p>
&lt;p>下面是这段输入的组成和来源。&lt;/p>
&lt;p>&lt;strong>1. System Prompt（系统提示词）&lt;/strong>&lt;/p>
&lt;p>系统提示词是 OpenClaw 每次调用模型都会带上的“基础说明书”。它包含了多段固定或半固定内容，核心目的是让模型知道它是什么、有哪些工具、在哪个工作目录、如何处理记忆和渠道消息等。&lt;/p>
&lt;p>System prompt 是一个“大盒子”，内部包含以下子块：&lt;/p>
&lt;ul>
&lt;li>Tooling 段：列出工具清单以及简要说明。&lt;/li>
&lt;li>Safety 段：基础安全与行为约束。&lt;/li>
&lt;li>Skills 段：技能目录提示（可很长，随技能数量增长）。&lt;/li>
&lt;li>Memory 段：记忆检索规范（告诉模型先搜索 MEMORY.md 等）。&lt;/li>
&lt;li>Documentation 段：本地文档路径提示。&lt;/li>
&lt;li>Workspace 段：当前工作目录 + 备注。&lt;/li>
&lt;li>Project Context 段：注入的上下文文件内容（通常是最大头）。&lt;/li>
&lt;li>Messaging/Voice/Reply Tags/Sandbox/Runtime 等辅助段。&lt;/li>
&lt;/ul>
&lt;p>下面的 2～4 小节之所以单独展开，是为了突出这些子块在 token 体积上的“贡献度”，而不是把它们当作 system prompt 之外的独立部分。&lt;/p>
&lt;p>System prompt 的构建逻辑集中在：&lt;/p>
&lt;ul>
&lt;li>&lt;code>src/agents/pi-embedded-runner/run/attempt.ts&lt;/code>&lt;/li>
&lt;li>&lt;code>src/agents/pi-embedded-runner/system-prompt.ts&lt;/code>&lt;/li>
&lt;li>&lt;code>src/agents/system-prompt.ts&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. Project Context（项目上下文注入）&lt;/strong>&lt;/p>
&lt;p>这是 token 消耗最明显的部分之一。OpenClaw 会在每次运行前读取一组 workspace 的“引导文件”，再将它们原文注入到 system prompt 中。&lt;/p>
&lt;p>默认会读取的文件包括：&lt;/p>
&lt;ul>
&lt;li>&lt;code>AGENTS.md&lt;/code>&lt;/li>
&lt;li>&lt;code>SOUL.md&lt;/code>&lt;/li>
&lt;li>&lt;code>TOOLS.md&lt;/code>&lt;/li>
&lt;li>&lt;code>IDENTITY.md&lt;/code>&lt;/li>
&lt;li>&lt;code>USER.md&lt;/code>&lt;/li>
&lt;li>&lt;code>HEARTBEAT.md&lt;/code>&lt;/li>
&lt;li>&lt;code>BOOTSTRAP.md&lt;/code>&lt;/li>
&lt;li>&lt;code>MEMORY.md&lt;/code> 或 &lt;code>memory.md&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>这些文件会被逐个裁剪（单文件上限默认 20,000 chars），然后以如下形式插入：&lt;/p>
&lt;pre tabindex="0">&lt;code># Project Context
## BOOTSTRAP.md
&amp;lt;内容&amp;gt;
&lt;/code>&lt;/pre>&lt;p>逻辑位于：&lt;/p>
&lt;ul>
&lt;li>&lt;code>src/agents/workspace.ts&lt;/code>（读取文件集合）&lt;/li>
&lt;li>&lt;code>src/agents/bootstrap-files.ts&lt;/code>（组装注入）&lt;/li>
&lt;li>&lt;code>src/agents/pi-embedded-helpers/bootstrap.ts&lt;/code>（裁剪）&lt;/li>
&lt;li>&lt;code>src/agents/system-prompt.ts&lt;/code>（最终注入）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. Skills 列表（技能清单）&lt;/strong>&lt;/p>
&lt;p>如果开启了 skills，OpenClaw 会把全部 skills 编成一个目录段落，注入到 system prompt。这个列表会随技能数量和描述长度线性增长。&lt;/p>
&lt;p>它不是“工具”，而是“工作流程说明”，模型会根据提示去读 SKILL.md。&lt;/p>
&lt;p>相关逻辑：&lt;/p>
&lt;ul>
&lt;li>&lt;code>src/agents/skills/workspace.ts&lt;/code>&lt;/li>
&lt;li>&lt;code>src/agents/system-prompt.ts&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>4. Tools 定义（工具清单 + schema）&lt;/strong>&lt;/p>
&lt;p>模型真正能调用的工具列表（如 &lt;code>read&lt;/code>、&lt;code>exec&lt;/code>、&lt;code>message&lt;/code>）会被注入到 system prompt 中，同时工具参数 schema 也会被传入模型。这部分通常不会在 prompt 里完整展示，但仍会占用输入 token。&lt;/p>
&lt;p>相关逻辑：&lt;/p>
&lt;ul>
&lt;li>&lt;code>src/agents/pi-tools.ts&lt;/code>&lt;/li>
&lt;li>&lt;code>src/agents/system-prompt.ts&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>5. 历史对话 + 工具结果&lt;/strong>&lt;/p>
&lt;p>除了 system prompt，OpenClaw 还会把 session 中的历史消息、工具调用结果一起发送给模型。这些历史会不断累积，直到触发 compaction 或 memory flush。&lt;/p>
&lt;p>相关逻辑：&lt;/p>
&lt;ul>
&lt;li>&lt;code>src/agents/pi-embedded-runner/run/attempt.ts&lt;/code>（加载历史）&lt;/li>
&lt;li>&lt;code>src/agents/pi-embedded-runner/compact.ts&lt;/code>（压缩）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>6. 当前用户输入&lt;/strong>&lt;/p>
&lt;p>最后才是用户的这一条消息，它会和上面所有内容一起构成“本次 API 调用的完整输入”。&lt;/p>
&lt;p>&lt;strong>总结：一次交互实际上不是“只发你这一句话”。&lt;/strong>&lt;/p>
&lt;p>OpenClaw 每次调用模型，会发送：&lt;/p>
&lt;ul>
&lt;li>system prompt（很长）&lt;/li>
&lt;li>project context（很长）&lt;/li>
&lt;li>skills 列表（可很长）&lt;/li>
&lt;li>工具 schema（可很长）&lt;/li>
&lt;li>历史对话与工具结果（会不断增长）&lt;/li>
&lt;li>当前用户消息&lt;/li>
&lt;/ul>
&lt;p>这就是为什么 token 会增长得很快。&lt;/p>
&lt;p>&lt;img src="https://caifengze.com/zh-cn/blog/2026/%E4%B8%BA%E4%BB%80%E4%B9%88-openclaw-%E6%B6%88%E8%80%97-token-%E8%BF%99%E4%B9%88%E5%BF%AB/overview.png"
width="4284"
height="2112"
srcset="https://caifengze.com/zh-cn/blog/2026/%E4%B8%BA%E4%BB%80%E4%B9%88-openclaw-%E6%B6%88%E8%80%97-token-%E8%BF%99%E4%B9%88%E5%BF%AB/overview_hu_e49c0d2ff66be2e5.png 480w, https://caifengze.com/zh-cn/blog/2026/%E4%B8%BA%E4%BB%80%E4%B9%88-openclaw-%E6%B6%88%E8%80%97-token-%E8%BF%99%E4%B9%88%E5%BF%AB/overview_hu_27464601b4dee01f.png 1024w"
loading="lazy"
alt="OpenClaw token flow overview"
class="gallery-image"
data-flex-grow="202"
data-flex-basis="486px"
>&lt;/p>
&lt;p>&lt;em>图：一次请求中 system prompt、session history、用户输入与子 agent/fallback/compaction 的关系。&lt;/em>&lt;/p>
&lt;p>&lt;strong>代码佐证：系统提示词的“总装配口”&lt;/strong>&lt;/p>
&lt;p>以下是 &lt;code>src/agents/pi-embedded-runner/run/attempt.ts&lt;/code> 中的关键片段，能直观看到哪些内容被打包进 system prompt（已做精简）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/agents/pi-embedded-runner/run/attempt.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">appendPrompt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">buildEmbeddedSystemPrompt&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">skillsPrompt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Skills 列表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">contextFiles&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Project Context（BOOTSTRAP 等）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">tools&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Tool 列表 + schema
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">docsPath&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 文档提示
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">runtimeInfo&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 运行环境信息
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">workspaceNotes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// workspace 提示
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">sandboxInfo&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 沙箱信息
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">modelAliasLines&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 模型别名
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ... 还有更多辅助信息
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="一次请求可能触发多轮-api工具调用链">一次请求可能触发多轮 API：工具调用链
&lt;/h2>&lt;p>一个“用户请求”不一定只调用一次模型 API。最常见的原因是 &lt;strong>工具调用链&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>模型看到工具列表后，先发起 tool call（如 &lt;code>read&lt;/code> / &lt;code>exec&lt;/code>）。&lt;/li>
&lt;li>OpenClaw 执行工具，产出 tool result。&lt;/li>
&lt;li>工具结果被写回 session（作为工具消息）。&lt;/li>
&lt;li>模型继续生成，必要时再发起下一次 tool call。&lt;/li>
&lt;/ol>
&lt;p>这意味着一次用户输入可能产生多轮模型交互，从而更快消耗 token。&lt;/p>
&lt;p>对应代码入口（精简路径）：&lt;/p>
&lt;ul>
&lt;li>工具定义：&lt;code>src/agents/pi-tools.ts&lt;/code>&lt;/li>
&lt;li>session 创建 + tools 注入：&lt;code>src/agents/pi-embedded-runner/run/attempt.ts&lt;/code>&lt;/li>
&lt;li>订阅工具事件：&lt;code>src/agents/pi-embedded-subscribe.ts&lt;/code>&lt;/li>
&lt;li>工具执行生命周期处理：&lt;code>src/agents/pi-embedded-subscribe.handlers.tools.ts&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="失败重试--fallback模型回退与重试如何发生">失败重试 / fallback：模型回退与重试如何发生
&lt;/h2>&lt;p>OpenClaw 并不是“遇错就算了”。当模型调用失败（超时、限流、鉴权等），系统会尝试&lt;strong>模型回退&lt;/strong>，继续用候选模型列表中的下一个模型重试。&lt;/p>
&lt;p>核心逻辑在 &lt;code>runWithModelFallback&lt;/code> 中实现：&lt;br>
&lt;code>src/agents/model-fallback.ts&lt;/code>&lt;/p>
&lt;p>它的逻辑非常清晰：&lt;/p>
&lt;ol>
&lt;li>先根据配置生成“候选模型列表”（来自 &lt;code>agents.defaults.model.fallbacks&lt;/code> 或调用时传入的 override）。&lt;/li>
&lt;li>按顺序逐个尝试调用模型。&lt;/li>
&lt;li>只有当错误被判定为 &lt;strong>FailoverError&lt;/strong> 才会进入下一轮回退；否则直接抛错。&lt;/li>
&lt;/ol>
&lt;p>这意味着，&lt;strong>一次用户请求可能因为回退机制而触发多次 API 调用&lt;/strong>。当然，这是好事。&lt;/p>
&lt;h2 id="compaction-与-memory-flush上下文快满时发生了什么">Compaction 与 Memory Flush：上下文快满时发生了什么
&lt;/h2>&lt;p>当上下文快满时，OpenClaw 可能触发&lt;strong>两种不同的额外模型调用&lt;/strong>：&lt;br>
一个是 &lt;strong>Compaction（摘要压缩）&lt;/strong>，一个是 &lt;strong>Memory Flush（记忆写入）&lt;/strong>。两者目的不同、流程也不同。&lt;/p>
&lt;h3 id="1-compaction摘要压缩">1. Compaction（摘要压缩）
&lt;/h3>&lt;p>目标：把历史对话“压缩成更短摘要”，释放上下文空间。&lt;/p>
&lt;p>实现逻辑在：&lt;code>src/agents/compaction.ts&lt;/code>&lt;/p>
&lt;p>关键点：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>不是一次性把全部历史发给模型&lt;/strong>&lt;br>
会先按 token 将历史分块，然后逐块调用模型生成摘要，再合并为一个总摘要。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>如果单条消息太大&lt;/strong>&lt;br>
会走 fallback：只摘要小消息，大消息用占位提示。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>简化流程：&lt;/p>
&lt;pre tabindex="0">&lt;code>历史消息 → 按 token 分块 → 每块调用模型摘要 → 合并摘要
&lt;/code>&lt;/pre>&lt;h3 id="2-memory-flush记忆写入">2. Memory Flush（记忆写入）
&lt;/h3>&lt;p>目标：在 compaction 之前，把“长期记忆”写入磁盘（&lt;code>memory/YYYY-MM-DD.md&lt;/code>）。&lt;/p>
&lt;p>触发逻辑在：&lt;code>src/auto-reply/reply/agent-runner-memory.ts&lt;/code>&lt;br>
提示与默认文案在：&lt;code>src/auto-reply/reply/memory-flush.ts&lt;/code>&lt;/p>
&lt;p>关键点：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>不是摘要历史&lt;/strong>&lt;br>
而是让模型整理出“值得长期保存的记忆”，写到 &lt;code>memory/&lt;/code> 目录。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>这是一轮额外的模型调用&lt;/strong>&lt;br>
目的在于保存持久记忆，而不是压缩上下文。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="子-agent--sessions_spawn并行与分工如何导致额外调用">子 agent / sessions_spawn：并行与分工如何导致额外调用
&lt;/h2>&lt;p>当任务复杂、耗时或适合并行时，主 agent 可能会调用 &lt;code>sessions_spawn&lt;/code> 工具，把子任务交给子 agent 处理。这会启动一个&lt;strong>全新的子会话&lt;/strong>，它拥有自己的 sessionKey、system prompt 和工具链路，因此会带来额外的模型调用。&lt;/p>
&lt;p>核心入口在：&lt;code>src/agents/tools/sessions-spawn-tool.ts&lt;/code>&lt;/p>
&lt;p>简化流程如下：&lt;/p>
&lt;ol>
&lt;li>主 agent 调用 &lt;code>sessions_spawn&lt;/code>（这是一种模型的工具调用选择）。&lt;/li>
&lt;li>系统生成 &lt;code>childSessionKey&lt;/code>（&lt;code>agent:&amp;lt;id&amp;gt;:subagent:&amp;lt;uuid&amp;gt;&lt;/code>）。&lt;/li>
&lt;li>构造子 agent 的 system prompt（&lt;code>buildSubagentSystemPrompt&lt;/code>）。&lt;/li>
&lt;li>通过 &lt;code>callGateway&lt;/code> 启动子 agent 的独立运行流程。&lt;/li>
&lt;li>子 agent 完成任务后，结果回传主会话。&lt;/li>
&lt;/ol>
&lt;p>简单来说，就是 &lt;strong>子 agent 是“另起一条完整会话”&lt;/strong>。因此它会显著增加整体的 API 调用次数与 token 消耗。&lt;/p></description></item><item><title>NebulaGraph × PyG</title><link>https://caifengze.com/zh-cn/blog/2025/nebulagraph-pyg/</link><pubDate>Mon, 18 Aug 2025 03:58:00 +0800</pubDate><guid>https://caifengze.com/zh-cn/blog/2025/nebulagraph-pyg/</guid><description>&lt;img src="https://caifengze.com/zh-cn/blog/2025/nebulagraph-pyg/cover.png" alt="Featured image of post NebulaGraph × PyG" />&lt;p>这篇文章更像一个“反思录”，记录一下整个 &lt;a class="link" href="https://github.com/Fengzdadi/nebula-pyg" target="_blank" rel="noopener"
>nebula-pyg&lt;/a> 的开发过程。&lt;/p>
&lt;h2 id="初逢">初逢
&lt;/h2>&lt;p>还是要先感谢 &lt;a class="link" href="https://summer-ospp.ac.cn/" target="_blank" rel="noopener"
>OSPP&lt;/a> 这个平台，让我遇见了 NebulaGraph 社区和 wey-gu老师。&lt;/p>
&lt;p>当然，项目还是需要竞争的，记得最初钟意的项目有 Kwok 的 “为 KWOK 添加 模拟真实 Pod 行为的策略配置”，以及现在就在做的
“NebulaGraph PyG Integration”。&lt;/p>
&lt;p>我是那种非常害怕竞争的人，要面试，讲想法（其实就是太菜了，如果你够了解，对这些东西应该是侃侃而谈的）。
即使我非常想尝试云原生相关的内容，最后因为觉得自己不熟练，放弃了，转而投向我更熟悉的 PyG 。&lt;/p>
&lt;p>现在去查了一下 四月份和 wey-gu老师 交流的邮件，来回总共七封，我竟然写了五封哈哈，其中第一封被错过了，大概十天后进行了 follow-up。得到了回复。
客观点来说，我现在看当时的邮件，感觉我像是早早滚蛋的样子。&lt;/p>
&lt;p>后来加上了X，老师的交流一下子就轻松了很多，简单说明了一下我的想法后，就开始等待第一次的线上交流。&lt;/p>
&lt;p>这次交流也是很意外的一件事哈哈，好像我并没有被通知到具体在几点参加会议，然后立马找了个咖啡厅进去，一脸懵逼的进，一脸懵逼的出。&lt;/p>
&lt;p>交流的时候，发现和我一起参与这个项目的两位也都很厉害，并且其中一位，在后续开发过程中，在一些相关搜索的时候，Google到了。
能通过他的Blog，他在准备的时候也做了好多相关工作。&lt;/p>
&lt;p>那我也是很辛运的一位了!&lt;/p>
&lt;p>（突然想知道为什么是我入选哈哈哈）&lt;/p>
&lt;p>接着就是长时间的等待结果了。录取！&lt;/p>
&lt;h2 id="设计">设计
&lt;/h2>&lt;p>这个项目的设计，整体上是参考了 &lt;a class="link" href="https://blog.kuzudb.com/post/kuzu-pyg-remote-backend/" target="_blank" rel="noopener"
>KUZU&lt;/a> 所做的，给 PyG 做 remote 适配的工作。&lt;/p>
&lt;p>框架大体上是相同的，在细节上会因为 nebulagraph 的特性做一些特殊处理。&lt;/p>
&lt;h2 id="框架">框架
&lt;/h2>&lt;p>总的来说，比较核心的点是实现 PyG 中的 &lt;code>FeatureStore&lt;/code> 和 &lt;code>GraphStore&lt;/code> 这两个接口。&lt;/p>
&lt;p>在上层，会为实例化这两个接口做一个额外的类，来满足一行代码就能返回两个实例，也就是现在的 &lt;code>NebulaPyG&lt;/code> 这个类。
&lt;code>NebulaPyG&lt;/code> 这个类还写了一个方法 &lt;code>create_snapshot()&lt;/code> 专门解决 &lt;a class="link" href="#vid" >vid&lt;/a> 问题。&lt;/p>
&lt;p>将 &lt;code>FeatureStore&lt;/code> 和 &lt;code>GraphStore&lt;/code> 这两个接口中的工具类通用化，放在了 utils.py 和 type_helper.py 的包中。
其中包括对特征的维度判断工具 &lt;code>get_feature_dim()&lt;/code>，为产生全局 snapshot 的 &lt;code>scan_all_tag_vids()&lt;/code> 等等。&lt;/p>
&lt;p>同时引入了一个 &lt;code>base_store&lt;/code>，给 &lt;code>FeatureStore&lt;/code> 和 &lt;code>GraphStore&lt;/code> 来继承，来专门处理 PyG 中的 &lt;code>DataLoader&lt;/code> 中的 &lt;a class="link" href="#%e5%a4%9a%e8%bf%9b%e7%a8%8b%e5%a4%84%e7%90%86" >多进程问题&lt;/a>。&lt;/p>
&lt;p>以下是整体上的类关系图。&lt;/p>
&lt;p>&lt;img src="https://caifengze.com/zh-cn/blog/2025/nebulagraph-pyg/1_overview_of_nebula-pyg_class_relationships.png"
width="3840"
height="2592"
srcset="https://caifengze.com/zh-cn/blog/2025/nebulagraph-pyg/1_overview_of_nebula-pyg_class_relationships_hu_c5d9d653c32a2341.png 480w, https://caifengze.com/zh-cn/blog/2025/nebulagraph-pyg/1_overview_of_nebula-pyg_class_relationships_hu_75a3f6c60855495d.png 1024w"
loading="lazy"
alt="1_overview_of_nebula-pyg_class_relationships.png"
class="gallery-image"
data-flex-grow="148"
data-flex-basis="355px"
>&lt;/p>
&lt;h2 id="细节">细节
&lt;/h2>&lt;h3 id="环境">环境
&lt;/h3>&lt;p>这是在开发 nebula-pyg 前遇到的一个很重大的问题。第一次发现自己对环境这么不熟练。&lt;/p>
&lt;p>第一个是环境管理，nebula-python 选用的是 &lt;a class="link" href="https://pdm-project.org/en/latest/" target="_blank" rel="noopener"
>pdm&lt;/a>，一个相对不错的包管理工具。
为什么说相对不错呢？&lt;/p>
&lt;ol>
&lt;li>pdm 并没有让我体会到方便，里面还是有挺多坑的。其最主要的功能还是弥补了传统的 &lt;code>venv&lt;/code> 等等没有的，依赖包的版本一致性问题（比如 nebula-pyg 依赖 A 包，A 包依赖 B 包，传统的 &lt;code>venv&lt;/code> 能指定 A 包的版本，但不能指定 B 包的版本，就会容易出现 A1 依赖 B 的 &lt;code>X.1-X.2&lt;/code> 版本，A2 依赖 B 的 &lt;code>X.2-X.3&lt;/code> 版本，这个时候 pdm 就能发挥作用把的 B 指定为 &lt;code>x.2&lt;/code> 版本）。这个功能可能因为我并不是多端协作开发，暂时没有深刻体会到。&lt;/li>
&lt;li>pdm 对 torch 的支持我觉得很一般，要自己进行配置，而且像 &lt;code>torch-vision&lt;/code> 之类的包，确实出了很奇怪的安装意外。印象中这个问题可能普便存在，之前使用 &lt;code>miniconda&lt;/code> 也出现过类似的问题。&lt;/li>
&lt;li>经过调研，和 wey-gu 老师的讨论，现在 uv 差不多要一统天下了（哪天我也去试试）。&lt;/li>
&lt;/ol>
&lt;p>第二个便是 nebulagraph 的环境。目前社区版的 nebulagraph 的 storaged 只能被同一网络下的 python interpretation 访问，不能被外部网路访问。
这就导致使用 nebula-pyg 需要将两者 nebulagraph 和 nebula-pyg 所使用的 python interpretation 安装在同一网络下。
并且 nebulagraph 仅能在 &lt;a class="link" href="https://docs.nebula-graph.com.cn/3.8.0/4.deployment-and-installation/1.resource-preparations/#_4" target="_blank" rel="noopener"
>指定的 linux 环境下安装&lt;/a>。&lt;/p>
&lt;p>那用户使用 nebula-pyg就有两种情况：&lt;/p>
&lt;ol>
&lt;li>使用 特定的 linux 系统，并在该环境下安装了 nebulagraph，解释器也运行在本地。&lt;/li>
&lt;li>使用 docker-compose 来启动 nebula-graph，然后将 python解释器 安装在同一网络中，通过 ssh 进行开发或者使用。注意：并不是很推荐使用 WSL，因为 WSL 存在严重的IO问题（wey-gu老师提醒的，并且在后续实践中确实出现了这个导致的 storaged 崩坏问题，以及文件的读写问题）&lt;/li>
&lt;/ol>
&lt;p>当然，以上推荐方式1。&lt;/p>
&lt;h3 id="vid">vid
&lt;/h3>&lt;p>nebulagraph 的 &lt;a class="link" href="https://docs.nebula-graph.com.cn/3.8.0/1.introduction/3.vid/" target="_blank" rel="noopener"
>VID&lt;/a> 支持 &lt;code>FIXED_STRING(&amp;lt;N&amp;gt;)&lt;/code> 和 &lt;code>INT64&lt;/code> 两种类型。
而 PyG，对 edge_index 有 &lt;code>{0,……，num_nodes-1}&lt;/code> 的&lt;a class="link" href="https://pytorch-geometric.readthedocs.io/en/latest/notes/introduction.html?highlight=only&amp;#43;hold&amp;#43;indice&amp;#43;range#:~:text=Note%20that%20it%20is%20necessary%20that%20the%20elements%20in%20edge_index%20only%20hold%20indices%20in%20the%20range%20%7B%200%2C%20...%2C%20num_nodes%20%2D%201%7D." target="_blank" rel="noopener"
>要求&lt;/a>。&lt;/p>
&lt;p>那这个转换到底要不要做呢，在 KUZU 的实现中，这个问题被留给了用户，用户在导入数据的时候，要求 index 必须满足上面的要求。
现在反思过来看，这个问题交给用户，也是一个不错的方案。仅从科研角度来看，市面上大多数的数据集中，index 都被限制在了这个范围。&lt;/p>
&lt;p>不过对于工业级别的数据，大多数都是基于一定规则的 uuid，或者基于 snowflake 生成的 id。特别是在分布式数据库中，大部分情况不会是自增的int。这个时候，VID 的映射就特别有必要了。&lt;/p>
&lt;p>在设计映射方案的时候，曾考虑过 KV-Cache，经过 wey-gu 老师的建议，主要结合了两个点：&lt;/p>
&lt;ol>
&lt;li>考不考虑动态场景？这个的回答是不太需要，即使是工业级别，snapshot 做一个 subgraph，速度还是非常快的。&lt;/li>
&lt;li>删除 tag 的时候连续序号需要怎么处理？这个在 KV-Cache 中非常难处理。&lt;/li>
&lt;/ol>
&lt;p>综合以上，采用了 Snapshot这个方案（go snapshot！），并使用 python 原生的 pickle 进行持久化。&lt;/p>
&lt;p>snapshot 产生的 pickle 文件 会包含有一下几个内容：&lt;/p>
&lt;ul>
&lt;li>&lt;code>vid_to_idx&lt;/code>&lt;/li>
&lt;li>&lt;code>idx_to_vid&lt;/code>&lt;/li>
&lt;li>&lt;code>vid_to_tag&lt;/code>：节点分类信息&lt;/li>
&lt;li>&lt;code>edge_type_groups&lt;/code>：三元组信息&lt;/li>
&lt;/ul>
&lt;p>前面两个看似重复的反向设计并不是冗余，而是 &lt;code>dict&lt;/code> 里面的 KV 对正向的查询更快，时间复杂度应该是 O(1)，如果在想通过 idx 去找 vid 的时候仅仅用 &lt;code>vid_to_idx&lt;/code> ，时间复杂度就会到达 O(n)。&lt;/p>
&lt;h3 id="featurestore-的扫描策略">FeatureStore 的扫描策略
&lt;/h3>&lt;p>大概在8月初的时候，我用了 nebulagraph 文档中 &lt;a class="link" href="https://docs.nebula-graph.com.cn/3.8.0/nebula-studio/quick-start/st-ug-plan-schema/#schema" target="_blank" rel="noopener"
>规划 Schema&lt;/a> 部分的示例数据集 basketballplayer 做完了所有的测试。
当全部测试跑通了以后，非常开心，已经早早的做下一步规划了。&lt;/p>
&lt;p>噩梦出现在决定做一个大规模数据集的案例。&lt;/p>
&lt;p>当时选用的是 &lt;a class="link" href="https://ogb.stanford.edu/docs/nodeprop/#ogbn-products" target="_blank" rel="noopener"
>ogbn-products&lt;/a>，我认为一个非常经典的例子。当我挂起他的时候，过了约莫一小时，并没有什么动静。
在当时的我看来应该是性能问题，可能会考虑到 WSL I/O，以及自己电脑性能。&lt;/p>
&lt;p>出去玩完约莫七个小时回到家后，看到还是一成不变的进度，我惊呆了，2,449,029 个 NODE 在我印象中并不会出现这么严重的性能问题，七个小时都读不完。
但我仍对数据集大小保持怀疑态度，于是，使用了&lt;a class="link" href="https://ogb.stanford.edu/docs/nodeprop/#ogbn-arxiv" target="_blank" rel="noopener"
>ogbn-arxiv&lt;/a>。并进行断点打印，
发现在读取数据中进行了无数轮次的&lt;code>scan_vertex_async&lt;/code>。那这是怎回事？&lt;/p>
&lt;p>我最初对 &lt;code>get_tensor&lt;/code> 的设计是调用 storaged 的 &lt;code>scan_vertex_async&lt;/code>（这个接口也是我 PR 到 nebula-python 中的，好像暂时没有更新）（更新：在10月18号成功&lt;a class="link" href="https://github.com/vesoft-inc/nebula-python/pull/377" target="_blank" rel="noopener"
>merge&lt;/a>！）。
这个接口做的工作是对图进行全局扫描，获得全局数据中指定 tag 的指定 prop。但它少了一个功能，就是获取指定 index 中的点。我在设计中对这个点的处理是，通过比较所需的 index 和 scan 出来的全图的点，
再将所需要点的 prop 进行返回。(后面就知道这是一个非常糟糕的设计了)&lt;/p>
&lt;p>我们来重新理一下流程，就知道，到底是什么地方出问题了。PyG 在使用 &lt;code>Neighborloader&lt;/code> 会先获取点的列表，根据 batch_size 这个参数，加载周围的点。
例如 &lt;code>batch_size = 32&lt;/code>。那就会从点的 List 里面获取 32 个点，然后再获取这 32 个点的邻点，将这些点作为一个 index，调用 &lt;code>get_tensor&lt;/code>。&lt;/p>
&lt;p>在 ogbn-arxiv 中，这个数量大约为 600 多个。&lt;em>进行 &lt;code>get_tensor&lt;/code> 的次数一般由 feat 数量（这是我踩的另外一个坑 &lt;a class="link" href="#get_tensor-%e4%b8%ad%e7%9a%84-xy-%e5%a4%84%e7%90%86" >get_tensor 中的 x,y 处理&lt;/a>），和进行几轮 epoch，几次batch训练决定的。&lt;/em>
所以使用 &lt;code>scan_vertex_async&lt;/code> 这样的全局扫描接口，从数据集中获取数据，每次都会扫描出 169,343 Node，和实际仅需的 600 多个 Node 进行比较一下就会发现，这是性能上的极大浪费。&lt;/p>
&lt;p>这时，我们来比较一下 KUZU的处理方式 和 wey-gu 老师之前为 dgl 写的 &lt;a class="link" href="https://github.com/wey-gu/nebula-dgl" target="_blank" rel="noopener"
>nebula-dgl&lt;/a>（刚刚才仔细的看了一下，之前只知道有这个项目哈哈哈）：&lt;/p>
&lt;ol>
&lt;li>KUZU: KUZU 很干脆，他的 &lt;code>scan_vertex&lt;/code> 函数中，有 indices 这个参数，能直接根据 indices(index) 直接从 storage 中获取指定 indice 的 prop。&lt;/li>
&lt;li>nebula-dgl：一般在训练的时候，dgl 会 scan 全图，读取data，生成这个图对象，之后的操作都是默认从图对象 g.ndata/edata 里按需切片。所以并不是涉及和 PyG 类似的频繁的读写。(这里觉得要是早点看到，也可以知道有query这个方案去针对小图，子图了)&lt;/li>
&lt;/ol>
&lt;p>对于方案一，我是觉得非常方便，只需要在 nebulagraph 的 stroaged 中加入一个算子即可。但这个方案被直接否决了。
反思一下，我记得不太清楚，wey-gu老师和我说的是 storaged 只适合做全图扫描，并不适合做条件查询。&lt;/p>
&lt;p>经过一些资料查证，首先是，nebulagraph 的 storaged 是基于 RocksDB（PS：有空可以研究一下 RocksDB！），有天然的顺序读（iterator）方案，直接具有全图遍历的功能。
第二个是，如果想实现这个功能，至少要做谓词求值、联合条件、选择最佳索引、回表取属性、聚合/去重/排序这些事情，从职责上来讲，这些都是 query 优化器的工作，最终还是应该由 graphd 来做。&lt;/p>
&lt;p>那最后也是在 wey-gu 老师的建议下，选择了 ngql 进行相关操作，并且 wey-gu老师有说，nebulagraph 对我描述的操作做了进一步的优化。
通过后续的测试发现也是，在 100,000 个级别的点 fetch 中，能够在 1.2-1.5s 左右返回所有数据。&lt;/p>
&lt;p>最终的解决方案也很简单，使用 &lt;a class="link" href="https://docs.nebula-graph.com.cn/3.8.0/3.ngql-guide/7.general-query-statements/4.fetch/" target="_blank" rel="noopener"
>fetch&lt;/a> 语句查询即可。
当然，还有 &lt;a class="link" href="https://docs.nebula-graph.com.cn/3.8.0/3.ngql-guide/7.general-query-statements/2.match/" target="_blank" rel="noopener"
>match&lt;/a> 方法。
match 更强大，比如能处理多跳子图等功能，但在性能上弱于&lt;code>fetch&lt;/code>，并且没有其他功能的需求，只是根据 &lt;code>index&lt;/code> 获取点的属性值，因此还是选用了&lt;code>fetch&lt;/code>。&lt;/p>
&lt;h3 id="get_tensor-中的-xy-处理">get_tensor 中的 x,y 处理
&lt;/h3>&lt;p>这个也是最初没有考虑到的问题。&lt;/p>
&lt;p>上面说的：&lt;/p>
&lt;blockquote>
&lt;p>进行 &lt;code>get_tensor&lt;/code> 的数量一般由 feat 数量……决定的。&lt;/p>&lt;/blockquote>
&lt;p>是因为在训练过程中，PyG 都会根据 &lt;code>get_all_tensor_attrs&lt;/code> 来获取属性列表，根据这个列表，在每一组 epoch 的每一次 batch 中都要对每一个存在于属性列表中的属性进行 &lt;code>get_tensor&lt;/code> 操作。
比如在 ogbn-arxiv 中，有 feat0，feat1 …… feat128， 这样 128 个属性列 和 label 标签列。
PyG就会使用 128+1 次的 &lt;code>get_tensor(attr=TensorAttr(group_name=&amp;quot;paper&amp;quot;, attr_name=&amp;quot;feat0&amp;quot;, index=...))&lt;/code> 进行特征/属性的获取。&lt;/p>
&lt;p>在上个部分，我每次都能看到 大约100多次训练以后就报错，我就开始怀疑，是 attr 部分的问题。因为 PyG 的模型训练，会要求存在 &lt;code>data.x&lt;/code> 和 &lt;code>data.y&lt;/code>。
但我并没有，我只有 feat0-feat128 和 label 列。&lt;/p>
&lt;p>那现在的任务就变成了 需要将 feat0-feat128 变为 &lt;code>data.x&lt;/code>，label 列变为 &lt;code>data.y&lt;/code>。&lt;/p>
&lt;p>（看别人的处理方法真的能学到很多）&lt;/p>
&lt;p>我看了 KUZU 对这个问题的解决方法，其实就是没有方法哈哈哈。因为其要求用户导入的特征数据就为&lt;a class="link" href="https://blog.kuzudb.com/post/kuzu-pyg-remote-backend/#:~:text=x%3A%20128%2Ddimensional%20node%20features%20%28so%20128%2Dsize%20float%20tensors%29" target="_blank" rel="noopener"
>多维度的 tensor float 类型&lt;/a>（应该是 vector 了）。&lt;/p>
&lt;p>那么解决这个问题的方案，就只能我们自己来想。&lt;/p>
&lt;p>因为 nebulagraph 并不支持 vector，更糟糕的是，也不支持&lt;a class="link" href="https://docs.nebula-graph.com.cn/3.8.0/3.ngql-guide/3.data-types/6.list/#:~:text=%E5%A4%8D%E5%90%88%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%EF%BC%88%E4%BE%8B%E5%A6%82%20List%E3%80%81Set%E3%80%81Map%EF%BC%89%E4%B8%8D%E8%83%BD%E5%AD%98%E5%82%A8%E4%B8%BA%E7%82%B9%E6%88%96%E8%BE%B9%E7%9A%84%E5%B1%9E%E6%80%A7%E3%80%82" target="_blank" rel="noopener"
>复合数据类型&lt;/a>（例如 List、Set、Map）。
所以在存数据的时候，和原来一样，只能存入 feat0-feat128 这样 128列 的特征向量。&lt;/p>
&lt;p>那第一个问题，就是这个 &lt;code>feature&lt;/code> 的合成以及 &lt;code>label&lt;/code> 的转换过程是由 nebula-pyg 来处理，还是用户自行处理。&lt;/p>
&lt;p>先来处理简单的，也就是 label。这个我认为在 nebula-pyg 中并不是很难解决的问题。&lt;/p>
&lt;p>为此，我引入了 &lt;code>Y_CANDIDATES&lt;/code> 这个常量，默认了 &lt;code>label&lt;/code>，&lt;code>y&lt;/code>，&lt;code>target&lt;/code>，&lt;code>category&lt;/code> 这几个标签为 &lt;code>data.y&lt;/code> 的标签。
在读取的过程中，能够直接将这个几个标签的特征量转换为 &lt;code>data.y&lt;/code>。&lt;/p>
&lt;p>接下来就是 &lt;code>feature&lt;/code> 的处理了。这里同样是交给用户还是 nebula-pyg 来解决的问题。主要考虑的点有以下几个：&lt;/p>
&lt;ol>
&lt;li>在我的经验，如果一些特征是经过特殊处理的，这些特殊处理的特征，很可能就是需要进行相当量的消融实验，而用户在拼接的过程中，只需要从 &lt;code>data.x = [data.feat0,……，data.feat128]&lt;/code> 中删除不需要的列即可。&lt;/li>
&lt;li>要用户自己进行拼接，即要用户多写两行代码，从使用体验上来说会非常的不佳。&lt;/li>
&lt;/ol>
&lt;p>既然两者都好，那就两者都要！&lt;/p>
&lt;p>那我们就需要进行一个模式区分，我引入了 &lt;code>expose&lt;/code> 这个参数，有两个 mode，&lt;code>x&lt;/code> 和 &lt;code>feats&lt;/code>，默认为 &lt;code>x&lt;/code>。当用户需要自行处理特征的时候，即可使用 &lt;code>feats&lt;/code>。&lt;/p>
&lt;p>&lt;code>x&lt;/code> 模式下的主要处理大致为，当调用 &lt;code>get_all_tensor_attrs&lt;/code> 会生成一个含有所有 &lt;code>feat&lt;/code> 的 &lt;code>dict&lt;/code>，并只返回 &lt;code>x&lt;/code>, &lt;code>y&lt;/code>。当 PyG 接收到 &lt;code>x&lt;/code> 后，会用这个 &lt;code>x&lt;/code> 作为调用 &lt;code>get_tensor&lt;/code> 的 &lt;code>attr.attr_name&lt;/code> 的参数，
&lt;code>get_tensor&lt;/code> 会自行进行识别，并读取之前所说的 &lt;code>dict&lt;/code>，然后一次返回含有所有 &lt;code>feat&lt;/code> 的 &lt;code>data.x&lt;/code>。&lt;/p>
&lt;p>（2:26am，作息真的很爆炸了，已经盖上电脑20min了，然后又把它打开，想一次性写完）&lt;/p>
&lt;h3 id="多进程处理">多进程处理
&lt;/h3>&lt;p>这一段内容其实也很长，我在完成这个事情的时候，觉得很值得，为此直接写了一篇doc，专门介绍这一部分内容，详细可以看 &lt;a class="link" href="https://github.com/Fengzdadi/nebula-pyg/blob/main/doc/Multi-process/Multi-process_zh-CN.md" target="_blank" rel="noopener"
>Multi-process description&lt;/a>
这里的 blog 也主要讲述的是心路历程，而不重在具体的解决细节。&lt;/p>
&lt;p>还记得 &lt;a class="link" href="#featurestore-%e7%9a%84%e6%89%ab%e6%8f%8f%e7%ad%96%e7%95%a5" >FeatureStore 的扫描策略&lt;/a> 这一部分引入了 &lt;code>query&lt;/code> 嘛。怎么说呢，这个工作真的给我带来了很大的麻烦。
一个问题的引入，引出了三个工程量都非常大的问题，&lt;a class="link" href="#featurestore-%e7%9a%84%e6%89%ab%e6%8f%8f%e7%ad%96%e7%95%a5" >FeatureStore 的扫描策略&lt;/a>，&lt;a class="link" href="#get_tensor-%e4%b8%ad%e7%9a%84-xy-%e5%a4%84%e7%90%86" >get_tensor 中的 x,y 处理&lt;/a> 和当下这个。&lt;/p>
&lt;p>印象中，引出这个问题的时候，是当我把 &lt;code>num_workers&lt;/code> 设置为非1，&lt;code>query&lt;/code> 的 &lt;code>resp&lt;/code> 就出现了各种串位。&lt;/p>
&lt;p>继续来讲问题的原因，在代码中，&lt;code>query&lt;/code> 是被 &lt;code>session&lt;/code>（&lt;code>graph client&lt;/code> or &lt;code>gclient&lt;/code>）执行的。而 PyG 的 &lt;code>DataLoader&lt;/code> 的 &lt;code>num_workers&lt;/code> 参数是可以开启多进程读数据的。此时，session 的独立性就尤为重要。&lt;/p>
&lt;p>nebulagraph 的 &lt;code>session&lt;/code> 并不安全，即使 nebula-python 还专门实现了 &lt;code>session_pool&lt;/code>，很可惜但也不可惜，可惜的是，wey-gu老师说这个&lt;code>session_pool&lt;/code>也不安全；不可惜的是，这是在我做完这部分工作以后才发现的，我也不想工作白费哈哈哈。&lt;/p>
&lt;p>最初的设计里，用户仅需初始化一个 gclient（连 graphd）和 sclient（连 storaged），后续所有的操作都是通过这两个对象。
这个问题前面不涉及频繁 &lt;code>query&lt;/code> 的时候并没有出现，因为之前的 &lt;code>scan&lt;/code> 全图，少说也要十几秒一次，并且每次的 &lt;code>resp&lt;/code> 都是相同的，即使串位了也无所谓。&lt;/p>
&lt;p>那问题解决的细节，我在此就不赘述，详细内容都在上面的 doc 中。我大概的提一下几个比较值得关注的点：&lt;/p>
&lt;ol>
&lt;li>PyTorch 中的 &lt;code>DataLoader&lt;/code> 是有 &lt;strong>Fork&lt;/strong> 和 &lt;strong>Spawn&lt;/strong> 两种模式的，如果是后者，可能都不需要我写这个算子来解决了。&lt;/li>
&lt;li>当前是通过工厂函数&amp;amp;懒加载的方案解决，我其实还在寻求更方便的模式，即使我已经写了一个工厂函数的utils，但还是没有commit，我觉得参数传递还是一个很大的问题，暂时没有找到很好的方案。&lt;/li>
&lt;li>并没有做内存占用的测试，这个应该如何进行？&lt;/li>
&lt;/ol>
&lt;hr>
&lt;p>至此，恭喜一下自己🎉，完成了一个鲁棒性不错，功能支持丰富的 nebula-pyg，至少我觉得比 KUZU 丰富一些。&lt;/p>
&lt;h2 id="余音">余音
&lt;/h2>&lt;p>写到这里，技术上的流水写完了。脑子里还有很多生活上以及后续的流水。&lt;/p>
&lt;p>非常感谢wey-gu老师，我还是想写写感谢在哪哈哈。&lt;/p>
&lt;p>就按时间线来讲吧，最开始我真的很害怕，因为我完全不知道 wey-gu老师 是怎么样的人，我的印象只有“怎么还不回我邮件”，“我真的要压上我全部家当在这个项目上吗？”。
这么焦虑的原因，可能也是因为我是很在意合作体验的人，我非常乐意请教合作，前提是，我内心要先认同，就是那种不能接受完全不熟的人来，如果这样，我会自己封闭自己，可能是 infj 的专属了。&lt;/p>
&lt;p>当加上了X进行简单交流以后，包括线上的一次见面会，我突然发现 WoW，好厉害，我真的能和这么厉害的人合作吗（直接怯战）。&lt;/p>
&lt;p>通过 iyear 的建议，努力给自己刷点存在感，做一些项目的前期调研，刷刷issue，有机会最好来几个PR，狠狠的补自己的项目书，也是不枉努力！&lt;/p>
&lt;p>直到下一件事发生前，我都会觉得，这个 mentor 非常的忙，但也很自由。自由的点在于我在想自己要不要做点日报，周报之类的，wey-gu老师 的反应，就是无所谓，这些都是冗余的。
忙在于，好像就是很忙哈哈。&lt;/p>
&lt;p>下一件事就是搭环境。其实这里心态已经崩溃了，我弄了一个多星期，竟然连环境都没搞定，我都不知道怎么开发。不过好消息是，wey-gu老师 直接就和我视频会议。
知道我还卡在这里了以后，先是给我解释了 nebulagraph 的框架，以及项目开发的大致方向，然后花了将近两小时，远程帮我配好了环境！并在过程中给我讲了很多我不知道的知识，技巧。&lt;/p>
&lt;p>打心底我对远程这个事情还是害怕的，一个是每次找 iyear 的时候，大部分情况的第一回答就是“远程解决不了”，而我在帮别人的时候，也会感受到远程的无力感。所以这次是小小的心灵感受到了充分的温暖哈哈。事后，我还狠狠的和 Hera 姐夸了。&lt;/p>
&lt;p>好像这件事之后，我们再也没有视频过了，一些问题也就是能很简单的通过电话以及文字解决（原来第一段视频远程是最后一段视频远程）。&lt;/p>
&lt;p>七月底，正好因为 VISA 的事，去了趟上海，（偷偷说Hera姐也很支持我去约饭）成功约上老师吃饭。当时也不是饭点，其实应该是我狠狠的在吃，老师我猜当时也不是很饿，陪我聊了两个小时。
对于我来说，真的收获了很多，见到了一些我曾经不知道的模式，更了解开源社区，之后，就会每天在X上闲逛，感觉这里面的人都好精彩。&lt;/p>
&lt;p>VISA的事真的很糟心，那几天的开发感觉就停滞了，后续也不见得进度快起来。老师也安慰了我挺多，但记住的就这样一句话，大概意思是，每天的活力有很多，不必为了这种结果不在你控制范围内的事消耗掉它，你还能用它去创造很多东西！&lt;/p>
&lt;p>突然觉得写到这也差不多，不必事无巨细，其他的，也就是会经常给我share一些开源的工作和工具，真的都很amazing。&lt;/p>
&lt;p>Hera姐也是人美心善的姐姐哈哈，因为（内心同意）这个问题，我就很想跑到余杭去看看 nebula 公司的情况，被Hera姐热情招待了！&lt;/p>
&lt;p>等我写完这个项目再回头看，项目真的不大，几千行的代码，甚至核心代码我感觉在2000行以内，剩下的都是 example，test和doc。但远远比之前写 CRUD 的成就感强多了。
包括但不限于学会以及更加了解 python，docker-compose，WSL，pdm等等，还有更强的独立思考，讨论，设计解决方案的能力（至少我现在是很骄傲的），探索各种工具的能力以及对开源社区充满了更多的好奇感，还给社区做出了1行代码的PR。&lt;/p>
&lt;p>戛然而止一下，再来换个话题哈哈。当这个项目真正步入尾声，内心还是些许沮丧的，当然也有开心的点！至少要结束了哈哈（不过不知道后续的维护压力大不大）。
我总是害怕自己从一个环境换到另一个环境，所以项目进入尾声的时候，就意味着，当下从非舒适圈到舒适圈的环境，又要变成了新的“非舒适圈”。&lt;/p>
&lt;p>此外，当再此听到需要将 repo 捐赠给社区的时候，兴奋带有少许难过。我记得 wey-gu老师 第一次和我说 repo 要捐给社区的的时候，真的非常愉悦，哇，我的 repo 也能被社区接受。
而现在的感觉是感觉像是自己养了两个月的，就要走了。BTW，其实开发中期的时候，一直觉得自己的代码很lese，也不会有人用，同类产品都有 KUZU了，为什么还会接受我的呢？
只能说被 wey-gu 老师鼓励了一下，nebulagraph 的客户都非常的大牌哈哈。突然觉得捐给社区这一个更大的平台，它的未来会更好！&lt;/p>
&lt;p>突然想到一件尴尬的事，每次和 wey-gu老师 聊天的时候，都会狠狠带上您，我想这应该是我从体制教育，传统文化培养下的，“典型的尊重长辈”。
然后就被点了——“不您的”，突然发现，您的疏远感确实好强，wey-gu老师 应该还是很乐意和我平等交流的。
再引申到 iyear 和我提过一次，在社区里，你身份地位是最不被care的，不论是地位，不论年龄，不论国籍，都能和进行你平等的交流。&lt;/p>
&lt;p>That‘s ALL！虽然我也不知道写完了没，但已经想睡觉了哈哈&lt;/p>
&lt;hr>
&lt;p>2025-08-18 03:58AM&lt;/p></description></item></channel></rss>