[{"content":"给人用的命令行和给 Agent 用的命令行，是两种东西。别慌——你不需要推翻重来，只需要加一张新脸。\n一个真实的尴尬场景 我有一个内部部署工具 idp-cli，专门给脚本和 AI Agent 调用（区别于交互式的 idp）。用法很直接：\nidp-cli deploy --app gms --branch master --platform all --isp all 看起来很\u0026quot;自动化友好\u0026quot;了对吧？JSON 输出、非交互、四个 flag 一传就走。\n但当我让 Claude Code 调用它时，事情开始变味：\nAgent 第一次调了 idp-cli deploy --app gms --branch main——少了两个必填 flag，直接报错 第二次它\u0026quot;学聪明了\u0026quot;，加上了 --platform production——但枚举值只有 test/sandbox/online，又错了 第三次终于对了，但 直接部署到了生产环境，没有任何确认机制 这个工具为\u0026quot;脚本\u0026quot;设计，不是为\u0026quot;会犯错的智能体\u0026quot;设计。\n2026 年 3 月，Google 的 Justin Poehnelt 写了一篇文章，标题是 \u0026ldquo;You Need to Rewrite Your CLI for AI Agents\u0026rdquo;。核心观点只有一句：\nHuman DX 优化的是\u0026quot;好找、容错\u0026quot;。Agent DX 优化的是\u0026quot;可预测、防得住\u0026quot;。\n这不是一个理论问题。2026 年初，Claude Code 和 Codex CLI 让\u0026quot;Agent 直接调用命令行工具\u0026quot;从实验变成了日常。当你团队里一半的 CLI 调用来自 Agent 而不是人，CLI 的设计假设就需要重新审视了。\n读完之后，我回头看自己写的一堆 CLI，发现每一个都中枪了。\n为什么不直接用 MCP / function calling，还走 CLI？因为 CLI 是存量最大的工具接口——绝大多数内部工具不会有 MCP server，JSON 入口是成本最低的\u0026quot;Agent 可调用化\u0026quot;方案。\n七个设计原则，逐条拆解 1. 一个 JSON 入口 \u0026gt; 一堆 Flag 人习惯一次记一个 flag，--name、--email、--role 分开传。 Agent 习惯直接把 API schema 映射成一个 JSON payload。\n对比我的 idp-cli：\n# 现状：Agent 要猜四个 flag 的名字和枚举值 idp-cli deploy --app gms --branch master --platform all --isp all # Agent 友好版：直接传 JSON，映射 API schema idp-cli deploy --json \u0026#39;{\u0026#34;app\u0026#34;:\u0026#34;gms\u0026#34;,\u0026#34;branch\u0026#34;:\u0026#34;master\u0026#34;,\u0026#34;platform\u0026#34;:\u0026#34;all\u0026#34;,\u0026#34;isp\u0026#34;:\u0026#34;all\u0026#34;}\u0026#39; 这不是\u0026quot;多此一举\u0026quot;。当 flag 有 10 个、其中 3 个有枚举约束、2 个互斥时，Agent 拼 flag 的出错率远高于拼一个 JSON。更关键的是，JSON payload 可以直接用 schema 验证——一行 jsonschema.validate() 搞定类型、枚举、必填的全量校验，而 flag 组合约束需要手写验证逻辑。这才是 JSON 入口的核心优势：可机器验证性。\n知名案例：Stripe CLI\nStripe 的设计哲学是\u0026quot;CLI 就是 API 的薄壳\u0026quot;——每个子命令直接映射一个 API endpoint，参数结构和 API 文档完全一致。Agent 不需要学一套\u0026quot;CLI 方言\u0026quot;，直接按 API 文档传参就行。\n2. Schema 自省 \u0026gt; --help 文本 人读 --help 觉得很自然。 Agent 读 --help 是在用 token 解析非结构化文本。\n我的 pis 工具（服务实例查询）的帮助输出长这样：\nUsage: pis query [service] [flags] Flags: -s, --service string Service key -g, --group string Group name --pick string Pick strategy: auto|random|primary-first --json Output JSON --pretty Pretty-print JSON Agent 要从这段文本里提取出\u0026quot;pick 参数的可选值是 auto、random、primary-first\u0026quot;，需要正则匹配或语义理解。浪费。\n如果改成：\npis schema query { \u0026#34;required\u0026#34;: [\u0026#34;service\u0026#34;], \u0026#34;properties\u0026#34;: { \u0026#34;service\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;BNS service key\u0026#34;}, \u0026#34;group\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;}, \u0026#34;pick\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [\u0026#34;auto\u0026#34;,\u0026#34;random\u0026#34;,\u0026#34;primary-first\u0026#34;,\u0026#34;primary-last\u0026#34;,\u0026#34;index\u0026#34;]} } } Agent 拿到 JSON schema，零歧义、零 token 浪费。\n知名案例：kubectl explain\nkubectl explain pod.spec.containers.resources 直接从 API Server 的 OpenAPI schema 拉取字段定义，包括类型、描述、是否必填。这是\u0026quot;活的文档\u0026quot;——永远和当前集群版本一致，不存在文档过期的问题。\nGoogle 的 gws（Workspace CLI）更进一步：启动时从 Google API Discovery Service 动态构建命令树，命令和参数永远和线上 API 一致。\n3. Skill File \u0026gt; Help Text 人通过 --help 和文档学习工具。 Agent 通过注入的上下文学习工具。\n--help 能告诉 Agent --env 接受 string，但不能告诉它只有两个合法值 online 和 shahe。这个 gap 就是 Skill File 要填的。\n我给 Claude Code 写了一系列 skill 文件（Markdown 格式），教它怎么用我的工具：\n# ES Log Query Skill ## 用法 query.py \u0026lt;service\u0026gt; \u0026lt;env\u0026gt; \u0026lt;keyword\u0026gt; [--hours N] [--size N] [--json] ## 不变量 - 默认查最近 1 小时，生产问题排查建议 --hours 4 - --size 超过 50 会导致 context 过大，建议 20 - 环境只有 online 和 shahe，没有 dev 关键是最后那段不变量——这不是帮助文档，而是 Agent 必须遵守的规则。\u0026ldquo;环境只有 online 和 shahe\u0026quot;这句话，阻止了 Agent hallucinate 出 --env production 或 --env staging。\nPoehnelt 在文章中建议 CLI 直接附带 SKILL.md 文件，编码 agent 级别的约束（如\u0026quot;所有 mutation 操作必须先 --dry-run\u0026quot;）。不管你用什么 Agent 框架，附带一个机器可读的约束文件都是好实践。如果你在用 Claude Code，你的 .claude/skills/ 目录就是 Skill File。\n4. 保护 Context Window：字段掩码 + 分页 Agent 的上下文窗口是有限资源。主流 LLM 的上下文窗口约 128k-200k token；200 个实例的完整 JSON 可能占 8-15k token，相当于把 Agent 的\u0026quot;工作记忆\u0026quot;吃掉 5-10%——留给推理和规划的空间就少了。\n我的 pis query --json 会返回每个实例的全部字段：\n{ \u0026#34;instances\u0026#34;: [ {\u0026#34;ip\u0026#34;:\u0026#34;10.1.2.3\u0026#34;,\u0026#34;port\u0026#34;:8080,\u0026#34;tag\u0026#34;:\u0026#34;primary\u0026#34;,\u0026#34;status\u0026#34;:\u0026#34;running\u0026#34;,\u0026#34;hostname\u0026#34;:\u0026#34;host-001\u0026#34;,\u0026#34;idc\u0026#34;:\u0026#34;gz\u0026#34;,\u0026#34;rack\u0026#34;:\u0026#34;A3\u0026#34;}, \u0026#34;... x200\u0026#34; ] } Agent 可能只需要 IP 和状态。但它没法说\u0026quot;只给我这两个字段\u0026rdquo;。\n应该怎么做：\n# 字段掩码：只返回 agent 需要的字段 pis query myservice --json --fields ip,status # NDJSON 分页：流式输出，agent 可以随时停 pis query myservice --json --ndjson 知名案例对比：\nCLI 字段过滤方式 gh pr list --json number,title --json 直接指定字段 kubectl get pods -o jsonpath='{.items[*].metadata.name}' JSONPath 表达式 gcloud compute instances list --format='json(name,status)' --format 内嵌字段列表 docker ps --format '{{.Names}} {{.Status}}' Go 模板 rg --json pattern NDJSON 流式输出，每条匹配一个 JSON 对象 ripgrep 的 NDJSON 模式尤其值得学习：每条匹配是独立的 JSON 对象（包含文件名、行号、匹配内容），Agent 可以逐条处理，不用等全部结果返回。Eclipse Theia IDE 就是从解析 rg 的人类输出切换到消费 rg --json 的。\n5. 输入强化：防的不是 Typo，是 Hallucination 人打错命令是少敲一个字母。Agent 打错命令是凭空编造一个看起来合理但不存在的参数。\n最常见的 hallucination 模式其实很日常：Agent 在别的项目里见过 production-bucket 这个名字，就\u0026quot;自信地\u0026quot;把它迁移到你的 bos_cli.py 里——但你的 bucket 叫 imeres。它不是恶意的，是把别处的经验当成了这里的事实。类似的：\n把枚举值 online 写成 production（在另一个项目里见过） 字段名 --app 写成 --name（和其他 CLI 混了） 多传一个工具不支持的 flag（从 --help 文本\u0026quot;推理\u0026quot;出来的） 更极端的情况还有路径遍历（../../etc/passwd）、控制字符注入、双重编码（%252F）等。\n这不是传统的安全加固（防恶意用户），而是防好心但不靠谱的队友。\n防御思路很直接——对所有 Agent 可调用的参数做 allowlist 校验：\nVALID_ENVS = {\u0026#34;online\u0026#34;, \u0026#34;shahe\u0026#34;} VALID_BUCKETS = {\u0026#34;imeres\u0026#34;, \u0026#34;imepri\u0026#34;, \u0026#34;imepub\u0026#34;} def validate_agent_input(env, bucket): if env not in VALID_ENVS: return {\u0026#34;error\u0026#34;: f\u0026#34;invalid env \u0026#39;{env}\u0026#39;, must be one of {VALID_ENVS}\u0026#34;} if bucket not in VALID_BUCKETS: return {\u0026#34;error\u0026#34;: f\u0026#34;invalid bucket \u0026#39;{bucket}\u0026#39;, must be one of {VALID_BUCKETS}\u0026#34;} if \u0026#34;..\u0026#34; in bucket or \u0026#34;\\x00\u0026#34; in bucket: return {\u0026#34;error\u0026#34;: \u0026#34;suspicious input detected\u0026#34;} 枚举值用 allowlist，路径参数做规范化检查。简单但有效。\n6. --dry-run：Agent 的确认按钮 人类可以看到交互式确认弹窗，犹豫一下再按回车。Agent 没有\u0026quot;犹豫\u0026quot;这个动作。\n这是我工具中最大的安全缺口。idp（交互版）有确认提示：\n即将部署 gms 到 all 平台，确认？[Y/n] 但 idp-cli（Agent 版）直接执行，没有任何安全缓冲。一次 hallucination 就是一次生产事故。\n应该怎么做：\nidp-cli deploy --json \u0026#39;{\u0026#34;app\u0026#34;:\u0026#34;gms\u0026#34;,\u0026#34;branch\u0026#34;:\u0026#34;master\u0026#34;,\u0026#34;platform\u0026#34;:\u0026#34;all\u0026#34;}\u0026#39; --dry-run { \u0026#34;would_deploy\u0026#34;: true, \u0026#34;app\u0026#34;: \u0026#34;gms\u0026#34;, \u0026#34;branch\u0026#34;: \u0026#34;master\u0026#34;, \u0026#34;affected_instances\u0026#34;: 42, \u0026#34;estimated_duration\u0026#34;: \u0026#34;8m\u0026#34;, \u0026#34;risk_level\u0026#34;: \u0026#34;high\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;deploying to all platforms\u0026#34; } Agent 先看到\u0026quot;会影响 42 个实例、风险高\u0026quot;，再决定是否真的执行。\n知名案例：kubectl 的两级 dry-run\n# 客户端验证：只检查 YAML 格式，不联系 API Server kubectl apply -f deploy.yaml --dry-run=client -o json # 服务端验证：发送到 API Server 做完整校验（admission controller、quota），但不持久化 kubectl apply -f deploy.yaml --dry-run=server -o json Terraform 更彻底——plan 和 apply 是两个独立命令，中间有一个人类必须审查的 diff。这个模式天然适合 Agent：先 terraform plan -out=tfplan，再 terraform show -json tfplan 让 Agent 审查变更，最后才 terraform apply tfplan。\n7. 同一个工具，人 / Agent / CI 三张脸 同一个能力，不同消费者需要不同的接口：\n消费者 接口 特点 人类 交互式 CLI 彩色输出、确认提示、自动补全 Agent 结构化 CLI 或 MCP JSON I/O、dry-run、schema 自省 CI/CD 环境变量 + 静默模式 headless auth、--quiet、退出码 我现在的做法是两个独立二进制：idp（人类版）和 idp-cli（Agent 版）。这能用，但维护成本翻倍。\n更好的做法是 一个二进制，根据上下文自动切换。GitHub CLI 已经这么做了：\n# 人类模式：彩色表格、截断长字段 gh pr list # 管道模式（检测到 stdout 不是 TTY）：自动切换为 tab 分隔、无颜色、不截断 gh pr list | head -5 # Agent 模式：JSON + 字段选择 + jq 过滤 gh pr list --json number,title --jq \u0026#39;.[].title\u0026#39; 一个二进制、零配置、根据调用上下文自动选择输出格式。优雅。\nTerraform 用环境变量实现类似效果：TF_IN_AUTOMATION=1 会抑制所有\u0026quot;人类引导文本\u0026quot;（比如 \u0026ldquo;Run terraform apply to\u0026hellip;\u0026quot;），让输出对程序友好。\n速查表：Human DX vs Agent DX 原则 Human DX（传统） Agent DX（新要求） 输入格式 多个便捷 flag 一个 --json payload，可 schema 验证 文档方式 --help 文本 运行时 Schema 自省 + Skill File 学习路径 读文档、试错 注入上下文不变量 输出控制 默认人类可读 JSON + 字段掩码 + NDJSON 分页 输入验证 防 typo 防 hallucination（allowlist + 路径规范化） 安全机制 确认提示 --dry-run 返回 plan 部署模式 一个二进制给人用 一个二进制，三张脸（人/Agent/CI） 常见误区 在动手改造之前，先避开几个坑：\n\u0026ldquo;加了 --json 就算 Agent 友好了\u0026rdquo;——输出 JSON 只是第一步。输入验证、dry-run、schema 自省都没做，Agent 照样会出事。 \u0026ldquo;Agent 会自己学会用工具\u0026rdquo;——不会。它会\u0026quot;自信地\u0026quot;用错。没有 Skill File 或 Schema 约束，Agent 就是在盲猜。 \u0026ldquo;安全问题是安全团队的事\u0026rdquo;——Agent hallucination 不是安全攻击，是日常使用中的常态。每个 CLI 作者都需要考虑。 改造路径：从哪里开始？ 不需要一次全改。Poehnelt 建议的渐进路径：\n第一步：加 --output json（几乎零成本） ↓ 第二步：严格验证输入（防 hallucination） ↓ 第三步：暴露 schema 自省接口 ↓ 第四步：实现字段掩码（--fields） ↓ 第五步：加 --dry-run ↓ 第六步：附带 Skill File + MCP 接口 对我自己来说，优先级很明确：先给 idp-cli 加 --dry-run。这是唯一有真实破坏力的 Agent 可调用工具，且改动量小——验证逻辑已经有了，只需要在最后一步前加一个\u0026quot;返回 plan 而非执行\u0026quot;的分支。预计半天工作量，改完之后 Claude Code 调用部署就从\u0026quot;盲射\u0026quot;变成\u0026quot;瞄准再打\u0026rdquo;。\n总结 CLI 设计正在经历一次范式转移。过去 40 年，我们围绕\u0026quot;人类在终端里打字\u0026quot;优化：彩色输出、交互补全、模糊匹配、友好的错误提示。这些对 AI Agent 全都没用，甚至有害。\nAgent 不需要你的输出好看，它需要你的输出可解析。 Agent 不需要你的错误提示友好，它需要你的输入验证严格。 Agent 不需要你的帮助文档详尽，它需要你的 schema 可查询。 Agent 不需要你的确认弹窗，它需要你的 --dry-run 真的能用。\n好消息是，这些改造大多是增量的——加一个 --json flag 不会破坏现有用户体验。你不需要重写 CLI，你需要给它加一张新脸。\n参考：\nJustin Poehnelt, \u0026ldquo;You Need to Rewrite Your CLI for AI Agents\u0026rdquo;, 2026-03 ","permalink":"https://fancive.github.io/posts/rewrite-cli-for-ai-agents/","summary":"\u003cp\u003e给人用的命令行和给 Agent 用的命令行，是两种东西。别慌——你不需要推翻重来，只需要加一张新脸。\u003c/p\u003e","title":"你的 CLI 该为 AI Agent 重写了"},{"content":"你记过的笔记，最后真的翻过几次？\n一个困扰所有笔记爱好者的问题 你可能试过 Notion、Obsidian、印象笔记，甚至纸质手账。\n刚开始总是兴致勃勃：精心设计分类、贴上标签、写好摘要。三个月后，收藏夹变成了垃圾场——笔记越来越多，但你再也不想打开它整理了。\n这不是你的问题，是所有人的问题。\n德国社会学家 Luhmann 用纸质卡片系统写了 50 本书。他的秘诀不是记得多，而是每张卡片都和其他卡片交叉引用。但维护这些引用极其耗时——他花了一辈子。\n2015 年之后，Notion、Obsidian、Roam Research 兴起，\u0026ldquo;第二大脑\u0026rdquo; 概念火了。但绝大多数人精心搭建的系统，几个月后就因为维护负担而被弃用。\n问题的本质是：建一个知识库很容易，养活它太难。\nKarpathy 的答案：让 AI 来养 2026 年 4 月，前 Tesla AI 主管、OpenAI 联合创始人 Andrej Karpathy 发了一条帖子，获得了 120 万次浏览。\n他说的事情很简单：\n\u0026ldquo;不要让 AI 帮你搜索，让 AI 帮你整理。\u0026rdquo;\n具体来说：\n你负责收集素材（文章、播客笔记、读书摘要） AI 负责把素材消化成一个结构化的个人百科 每次你提一个问题，AI 不仅回答，还把答案写回百科里 AI 定期检查百科的质量：有没有过时的信息？有没有矛盾？有没有遗漏？ 他把这叫做 \u0026ldquo;编译知识\u0026rdquo; —— 不是每次提问都从头查找（那是 ChatGPT 做的事），而是把知识整理一次，然后持续维护和更新。\n用得越多，知识库越丰富。这就是\u0026quot;知识复利\u0026quot;。\n它长什么样？ 想象你有一个三层书架：\n第一层：原始素材（你来放）\n你剪藏的文章、看过的书、听过的播客笔记。这些是\u0026quot;原材料\u0026quot;，AI 只读不改。\n第二层：个人百科（AI 来维护）\nAI 把原始素材消化后，生成一个个\u0026quot;百科词条\u0026quot;。比如你读了一篇关于睡眠的文章，AI 会：\n创建一个\u0026quot;睡眠科学\u0026quot;词条 如果已有\u0026quot;睡眠科学\u0026quot;词条，就把新信息追加进去 自动链接到相关词条（比如\u0026quot;精力管理\u0026quot;、\u0026ldquo;冥想\u0026rdquo;） 第三层：规则手册（你来定义，AI 来遵守）\n一份纯文本文件，告诉 AI：你的百科按什么分类？什么算一个\u0026quot;词条\u0026quot;？什么格式？什么该链接什么？\n这份规则手册就是你对知识的理解方式——也是整个系统中人类不可替代的部分。\nAI 做的五件事 1. 消化（Ingest） 你扔进一篇文章，AI 读完后不是简单存起来，而是：\n提取 3-8 个核心概念 为每个概念创建或更新百科词条 建立词条之间的交叉链接 把原始文章归档 一篇文章进去，十几个词条被更新。\n2. 提问回写（Query \u0026amp; File-Back） 你问 AI 一个问题，AI 从百科里综合回答。关键是：答案会被写回百科，成为新的词条。\n下次你或别人问类似的问题，答案已经在那里了。每次提问都是在给知识库\u0026quot;充值\u0026quot;。\n3. 质量检查（Lint） AI 定期扫描百科，检查：\n有没有两个词条说法矛盾？ 有没有引用了过时的数据？ 有没有被提到很多次但还没有单独词条的概念？ 就像给你的知识库做\u0026quot;体检\u0026quot;。\n4. 交叉链接（Cross-Link） AI 会自动发现\u0026quot;这两个词条虽然在不同领域，但其实有关系\u0026quot;，然后建立链接。\n比如\u0026quot;多巴胺系统\u0026quot;（心理学）和\u0026quot;习惯养成\u0026quot;（方法论）之间的关联。你可能不会手动去链，但 AI 会。\n5. 结构重组（Compile） 随着百科越来越大，AI 会建议：\n这两个词条内容重叠了，应该合并 这个词条太长了，应该拆分 这个词条放错了分类 然后你确认，AI 执行。就像给书架定期整理。\n我是怎么做的：5800 篇笔记的真实经历 说了这么多理论，我拿自己的笔记库举个例子。\n我从 2019 年开始用 Obsidian 记笔记。技术、心理学、投资、育儿、冥想、读书——什么都往里扔。七年下来攒了 5800 多篇。\n老实说，2024 年之前它就是一个高级收藏夹。东西是存了，但要找什么，全靠记忆和搜索框。很多笔记写完就再也没打开过。\n2025 年我开始用 AI（Claude Code）直接操作这个笔记库。一开始只是让它帮忙整理格式，后来逐步演化出了上面说的五大操作。我不是按照 Karpathy 的方案搭的——是自然长出来的，直到看到他的帖子才发现我们做了几乎一样的事。\n消化一篇文章，实际发生了什么 上周我看了一期 Huberman Lab 的播客，关于多巴胺系统。我把笔记扔进 Obsidian 的收件箱，然后对 AI 说\u0026quot;消化这篇\u0026quot;。\nAI 读完后告诉我：\n📄 素材：多巴胺 动力、专注力\u0026amp;满足感 📂 领域：心理学、健康 💡 核心概念：多巴胺基线/峰值动态、间歇性强化、冷水暴露 📝 建议新建：多巴胺系统 🔄 建议更新：多巴胺重置、动机\n我确认后，它一口气更新了 5 个文件：新建了\u0026quot;多巴胺系统\u0026quot;词条，更新了已有的\u0026quot;动机\u0026quot;页面，在索引里添加了条目，还把原始笔记归了档。\n一篇播客笔记，变成了知识网络里的一个节点。\n我的知识库长什么样 打开我的 知识管理/ 目录，是这样的结构：\n知识管理/ ├── 技术人生/ ← 编程、AI、职业发展 ├── 商业经济/ ← 投资、FIRE 规划、宏观经济 ├── 心理/ ← 动机、情绪调节、冥想 ├── 方法论/ ← 效率、沟通、认知 ├── 健康与医疗/ ← 睡眠、运动、营养 ├── 爱与家庭/ ← 育儿、夫妻关系 ├── 数学/ ← 线性代数、微积分 ├── 历史/ ← 中国史、经济史 ├── ... (11 个领域) ├── index.md ← AI 用这个文件导航整个百科 └── ingest-log.md ← 每次操作的日志 其中 index.md 是整个系统的导航枢纽。AI 每次操作前先读它，就知道有哪些词条、在哪个领域、是什么类型：\n- [[多巴胺系统]] [concept] — 基线/峰值动态、间歇性强化、14 个优化工具 - [[睡眠优化与生物节律]] [synthesis] — 睡眠分期、深睡眠功能、REM - [[技术领导力]] [concept] — IC→TL 转变、SBI 反馈模型、1on1 - [[家庭理财规划]] [synthesis] — 双职工三口之家理财深度报告 300 多个词条，每个后面都有一句话摘要。AI 靠这个就能找到任何知识，不需要什么向量数据库。\n给知识库做\u0026quot;体检\u0026quot; 上周我第一次跑了全量\u0026quot;体检\u0026quot;（Lint），扫描了将近 1000 个页面。结果让我吃了一惊：\n过时内容：11 处（引用了 2023 年的数据，但已有更新研究） 缺失交叉引用：7 对（应该互相链接但没有的词条） 孤立页面：约 150 个（没有任何其他页面链接到它们） 150 个孤立页面——这就是手动维护知识库的代价。你以为写完了就行了，其实没人（包括你自己）会再去给它们建立连接。\nAI 自动修复了 7 对缺失的交叉链接，比如把\u0026quot;习惯优化\u0026quot;和\u0026quot;拖延症\u0026quot;连起来——它们本来就该互相引用，但我写的时候没想到。\n结构重组：993 页大扫除 体检发现问题后，我又跑了一次\u0026quot;结构重组\u0026quot;（Compile）。AI 扫描了 993 个页面，给出了一份重组方案：\n合并：两个\u0026quot;可观测性工程\u0026quot;词条内容 70% 重叠 → 合并为一个 搬家：一个关于\u0026quot;工程师情绪调节\u0026quot;的词条放在了\u0026quot;健康\u0026quot;目录下 → 移到\u0026quot;心理\u0026quot; 改名：\u0026ldquo;iv 准备\u0026rdquo; → \u0026ldquo;面试准备\u0026rdquo;（谁看得懂 iv 是什么） AI 列出方案，我逐条确认，它再执行。全程 20 分钟，手动做这些至少要半天。\n一点私人感受 说实话，刚开始让 AI 整理笔记的时候，有一种奇怪的感觉——像是把日记交给了别人保管。\n后来想明白了：AI 整理的是结构，不是想法。 我的日记、反思、和妻子吵架后的复盘——这些 AI 不碰，也不该碰。它只负责把我读过的文章、学过的东西变成随时可以查阅的百科。\n我每天的日记模板里追踪睡眠时长、能量水平、专注度、运动数据。每周 AI 自动聚合成周报。知识管理只是这个系统的一部分，它延伸到了整个生活的量化和反思。\n这不是效率工具。这是一面镜子。\nBefore \u0026amp; After：到底改变了什么 用了一年多，我可以很具体地说出前后差别：\n以前（纯 Obsidian，没有 AI）：\n看完一篇好文章，花 20 分钟写摘要、打标签、想该放哪个文件夹。大部分时候懒得做，直接扔收件箱里腐烂。 想找之前看过的某个观点，靠关键词搜索碰运气。搜到了算赚到，搜不到就当没看过。 笔记之间几乎没有连接。\u0026ldquo;多巴胺\u0026quot;和\u0026quot;习惯养成\u0026quot;明明有关系，但我从来没手动链过——因为链完这两个，还有几百个类似的关系等着我。 每隔几个月，对着杂乱的笔记库叹口气，下决心要\u0026quot;好好整理一下\u0026rdquo;。整了两个小时，累了，放弃。 知识是平的。5800 篇笔记像 5800 张散落的纸，没有结构，没有层次，搜索是唯一的入口。 现在（Obsidian + AI 维护）：\n看完一篇文章，对 AI 说\u0026quot;消化这篇\u0026quot;。两分钟后，5-15 个词条被创建或更新，交叉链接自动建好，索引自动更新，原文自动归档。我什么都不用想。 想找什么，问 AI 就行——它不仅能搜索，还会跨多个词条综合回答，而且答案会被存回百科，下次不用重新推导。 笔记之间有了密集的连接。 心理学和方法论之间、投资和职业规划之间——AI 帮我发现了大量我自己不会注意到的关联。知识变成了一张网，而不是一堆纸。 不需要\u0026quot;好好整理\u0026quot;了。AI 定期做质量检查（发现过时信息、矛盾、孤立页面），定期做结构重组（合并重叠、调整分类）。维护是自动的。 知识有了纵深。 11 个领域、300 个索引词条、每个词条有来源追溯和交叉链接。不是我记了 5800 篇笔记，是我有了一个 993 页的个人百科。 如果用数字来说：\n以前 现在 消化一篇文章 20 分钟（经常放弃） 2 分钟（AI 做） 找一个曾经看过的观点 靠搜索碰运气 问 AI，秒级综合回答 笔记之间的交叉链接 几乎为零 自动维护，每周新增 20+ 条 \u0026ldquo;整理笔记\u0026rdquo; 每隔几个月痛苦一次 不需要了，AI 持续维护 知识库可用性 收藏夹级别 可查询的个人百科 最本质的变化是：笔记从\u0026quot;写完就死\u0026quot;变成了\u0026quot;越用越活\u0026quot;。\n以前每篇笔记的生命周期是：写 → 存 → 遗忘。现在是：写 → AI 消化进百科 → 被其他词条引用 → 被提问时调出 → 被新素材更新 → 被质量检查验证。每篇笔记都在持续参与知识循环。\n谁在这么做？ Andrej Karpathy（前 Tesla AI 主管）—— 提出了这个模式，但只发布了理念描述，没有发布代码。他说：\u0026ldquo;在 AI 时代，模式比实现更有价值。\u0026rdquo;\nTiago Forte（《打造第二大脑》作者）—— 2026 年把他的\u0026quot;第二大脑\u0026quot;方法论升级为 AI 版本。他务实地承认：\u0026ldquo;从来没有说的那么快或那么容易。\u0026rdquo;\nSteph Ango（Obsidian CEO）—— 发布了官方的 AI 技能包（20,000+ 星标），正式拥抱了这个方向。这不再是极客的小众玩法。\n和 ChatGPT / NotebookLM 有什么不同？ 一句话：ChatGPT 是一次性的，AI 百科是积累的。\n你在 ChatGPT 里问了一个很好的问题，得到了一个很好的回答。然后呢？聊天记录沉到历史里，下次你忘了，又问一遍。\nGoogle NotebookLM 好一些——它基于你上传的文档回答。但它也不会帮你整理文档、建立交叉链接、或者检查内容质量。\nAI 百科的核心区别是复利：每次使用都让系统变得更好。不是你在消费知识，而是你在生产知识。\n你需要什么工具？ 如果你想尝试，有两个路线：\n想自己折腾的（最灵活） Obsidian（免费笔记工具）+ Claude Code（AI 命令行工具） 写一份 CLAUDE.md 规则文件 开始收集素材，让 AI 消化 这是灵活度最高但上手门槛也最高的方案。\n不想折腾的（开箱即用） 工具 特点 Mem.ai AI 自动整理笔记，不用手动分类 Reflect 理解笔记之间的关系，端到端加密 Khoj 开源免费，可以自己托管 Notion AI 如果你的团队已经在用 Notion 需要警惕的三件事 1. \u0026ldquo;我以为我懂了\u0026rdquo; AI 帮你写了一篇完美的知识总结。你读了一遍，觉得自己理解了。但那个理解在 AI 的词条里，不在你的脑子里。\n心理学研究发现，人们系统性地高估自己对 AI 生成内容的理解程度。这叫\u0026quot;能力幻觉\u0026quot;。\n应对：重要的领域，先自己读原文，再让 AI 整理。不要跳过阅读这一步。\n2. AI 写错了怎么办？ 聊天窗口的错误你看一眼就过去了。但如果错误被写进了百科，它会影响后续所有的查询和综合。\n应对：保留原始素材（AI 不能修改），定期做质量检查，操作日志可追溯。\n3. 系统越来越大，AI 还跟得上吗？ Karpathy 建议百科规模在 ~100 篇文章（约 40 万字）以内效果最好。超过这个规模，可能需要搜索技术的辅助。\n应对：先从一个领域开始，不要一上来就想整理所有笔记。\n为什么这件事现在值得关注？ 过去 30 年，个人知识管理经历了三个阶段：\n阶段 代表 问题 纸质卡片 Luhmann 维护成本极高，一般人做不到 数字笔记 Notion/Obsidian 建的时候很爽，几个月后弃用 AI 维护的百科 Karpathy 模式 维护成本趋近于零 第三阶段解决了前两个阶段的核心问题：不是帮你记更多，而是帮你把已经记的东西养活。\nObsidian 的 CEO 发布官方 AI 技能包、Karpathy 的帖子获得百万浏览、多个开源项目在一周内涌现 —— 这些信号都在说：个人知识管理的 AI 时代已经开始了。\n你的笔记不再需要你亲自整理。但你读什么、问什么、关注什么 —— 这些依然只有你能决定。\nAI 是园丁，但花园的主人是你。\n参考来源：Karpathy LLM Wiki (GitHub Gist) | Obsidian Skills (kepano) | Tiago Forte - AI Second Brain | 30+ 学术和行业来源。完整研究报告见原文。\n","permalink":"https://fancive.github.io/posts/agentic-wiki-ai-personal-encyclopedia/","summary":"\u003cp\u003e你记过的笔记，最后真的翻过几次？\u003c/p\u003e","title":"让 AI 帮你维护一个个人百科全书"},{"content":"背景 在微服务架构中，一个服务通常包含数十甚至上百个 API 接口。当我们需要优化 CPU 资源消耗时，往往面临一个问题：哪些接口消耗了最多的 CPU？\n传统方法是通过 profiling 工具（如 pprof）分析，但这种方法：\n只能看到函数级别的消耗，难以直接关联到接口 需要在生产环境开启采样，有一定性能开销 难以持续监控和量化 本文介绍一种基于线性回归的方法，利用现有的监控数据（QPS 和 CPU 使用率），估算每个接口的 CPU 贡献。\n核心思路 基本假设 假设服务的 CPU 使用率可以表示为各接口 QPS 的线性组合：\nCPU% = β₀ + β₁×QPS₁ + β₂×QPS₂ + ... + βₙ×QPSₙ + ε 其中：\nβᵢ 是接口 i 的单请求 CPU 成本（每处理一个请求消耗的 CPU 百分比，例如 β=0.001 表示每个请求消耗 0.001% CPU） β₀ 是截距，表示基础 CPU 消耗（与请求无关的开销，如 GC、定时任务等） ε 是误差项 单位说明：本文中 CPU% 范围为 0-100，表示单核 CPU 的使用百分比。如果服务运行在多核机器上，需要先将监控数据归一化到单核。\n为什么可行？ 线性可加性：在高并发场景下，CPU 消耗近似与请求量成正比 数据可得：QPS 和 CPU 使用率是最常见的监控指标 可解释性：回归系数直接表示\u0026quot;单请求成本\u0026quot;，业务含义清晰 注意：相关性 ≠ 因果性 回归分析只能揭示相关关系，不能证明因果。如果有外部因素同时影响 QPS 和 CPU（如流量高峰时段），系数可能存在偏差。因此，分析结果应作为优化方向的参考，具体接口仍需结合代码审查确认。\n实现步骤 Step 1: 数据采集 从监控系统获取时序数据：\nimport pandas as pd import numpy as np # 伪代码：从 Prometheus 获取数据 qps_data = prometheus.query( \u0026#39;sum(rate(http_requests_total[5m])) by (api)\u0026#39;, start_time, end_time, step=\u0026#39;5m\u0026#39; ) cpu_data = prometheus.query( \u0026#39;avg(cpu_usage_percent)\u0026#39;, start_time, end_time, step=\u0026#39;5m\u0026#39; ) # 转换为 DataFrame # X: 特征矩阵，每列是一个接口的 QPS，每行是一个时间点 # y: 目标向量，CPU 使用率 X = pd.DataFrame(qps_data).pivot(index=\u0026#39;timestamp\u0026#39;, columns=\u0026#39;api\u0026#39;, values=\u0026#39;qps\u0026#39;) y = pd.Series(cpu_data[\u0026#39;cpu_percent\u0026#39;], index=X.index) 关键点：\n时间对齐：QPS 和 CPU 数据的时间戳必须一致 采样间隔：建议 5-10 分钟，太短噪声大，太长样本少 数据量：至少 500+ 个时间点，保证回归的稳定性 多实例聚合：如果服务有多个实例，需将 QPS 和 CPU 分别求和/平均后再分析 Step 2: 数据预处理 # 处理缺失值 X = X.fillna(0) y = y.fillna(y.mean()) # 过滤零值过多的接口（\u0026gt;90% 为零的接口剔除） non_zero_ratio = (X \u0026gt; 0).mean() valid_cols = X.columns[non_zero_ratio \u0026gt; 0.1] X = X[valid_cols] print(f\u0026#34;保留 {len(valid_cols)} 个接口，剔除 {len(non_zero_ratio) - len(valid_cols)} 个低频接口\u0026#34;) 为什么要过滤零值过多的接口？\n低频接口的数据稀疏，回归系数不稳定，置信区间很宽，结论不可靠。\nStep 3: 回归建模 使用 Ridge 回归（L2 正则化）而非普通最小二乘：\nfrom sklearn.linear_model import RidgeCV from sklearn.model_selection import train_test_split, TimeSeriesSplit # 划分训练集和测试集，用于验证模型泛化能力 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False # 时序数据不打乱 ) # Ridge 回归，自动选择最优正则化参数 # 注意：使用 TimeSeriesSplit 保持时序顺序，避免数据泄漏 model = RidgeCV( alphas=[0.001, 0.01, 0.1, 1.0, 10.0], cv=TimeSeriesSplit(n_splits=5) ) model.fit(X_train, y_train) # 评估模型 train_r2 = model.score(X_train, y_train) test_r2 = model.score(X_test, y_test) print(f\u0026#34;训练集 R²: {train_r2:.4f}, 测试集 R²: {test_r2:.4f}\u0026#34;) print(f\u0026#34;最优 alpha: {model.alpha_}\u0026#34;) # 系数即为各接口的单请求 CPU 成本 coefficients = dict(zip(X.columns, model.coef_)) 为什么用 Ridge？\n共线性缓解：当接口 QPS 存在相关性时，Ridge 能给出更稳定的系数估计 过拟合风险：接口数量多时，普通回归容易过拟合 系数稳定性：正则化使系数更稳定 处理负系数：\n理论上，单请求 CPU 成本不应为负。如果出现负系数，可能原因：\n共线性：与其他接口高度相关，系数被\u0026quot;分摊\u0026quot; 数据噪声：低频接口数据不足 遗漏变量：存在未建模的因素 建议将负系数视为 0（无显著贡献），或使用非负最小二乘（scipy.optimize.nnls）。\nStep 4: 置信区间估计（Block Bootstrap） 由于时序数据存在自相关，传统的 Bootstrap 会低估方差。使用 Block Bootstrap：\nfrom sklearn.linear_model import Ridge def block_bootstrap_ci(X, y, alpha, block_size=10, n_iterations=100, random_state=42): \u0026#34;\u0026#34;\u0026#34; 使用 Block Bootstrap 计算回归系数的置信区间 参数: X: 特征矩阵 (DataFrame) y: 目标向量 (Series) alpha: Ridge 正则化参数 block_size: 块大小，建议为自相关衰减长度 n_iterations: Bootstrap 迭代次数 random_state: 随机种子，保证结果可复现 返回: lower, upper: 95% 置信区间的下界和上界 \u0026#34;\u0026#34;\u0026#34; rng = np.random.default_rng(random_state) n_samples = len(y) # 使用 ceiling division 确保覆盖所有样本 n_blocks = (n_samples + block_size - 1) // block_size coefficients_list = [] for _ in range(n_iterations): # 随机选择块（有放回） block_indices = rng.choice(n_blocks, n_blocks, replace=True) sample_indices = [] for idx in block_indices: start = idx * block_size end = min((idx + 1) * block_size, n_samples) sample_indices.extend(range(start, end)) # 每次迭代创建新的模型实例 boot_model = Ridge(alpha=alpha) X_boot = X.iloc[sample_indices].values y_boot = y.iloc[sample_indices].values boot_model.fit(X_boot, y_boot) coefficients_list.append(boot_model.coef_) # 计算 95% 置信区间 coefficients_array = np.array(coefficients_list) lower = np.percentile(coefficients_array, 2.5, axis=0) upper = np.percentile(coefficients_array, 97.5, axis=0) return lower, upper # 使用示例 lower, upper = block_bootstrap_ci(X_train, y_train, alpha=model.alpha_) ci = {col: (lower[i], upper[i]) for i, col in enumerate(X_train.columns)} 置信区间下界 \u0026gt; 0 的接口，我们认为其系数是统计显著的。\nStep 5: 计算 CPU 贡献 # 计算各接口的平均 QPS avg_qps = X.mean() # 计算 CPU 贡献 = 系数 × 平均 QPS contribution = { api: max(0, coefficients[api]) * avg_qps[api] # 负系数视为 0 for api in X.columns } # 按贡献排序 top_consumers = sorted(contribution.items(), key=lambda x: x[1], reverse=True) # 输出 Top 10 print(\u0026#34;CPU 贡献 Top 10:\u0026#34;) for api, contrib in top_consumers[:10]: coef = coefficients[api] qps = avg_qps[api] is_significant = ci[api][0] \u0026gt; 0 print(f\u0026#34; {api}: 系数={coef:.6f}, QPS={qps:.1f}, 贡献={contrib:.2f}%, 显著={\u0026#39;✓\u0026#39; if is_significant else \u0026#39;✗\u0026#39;}\u0026#34;) 结果解读 输出示例 接口 单请求成本 平均 QPS 总贡献(%) 显著性 /api/user/info 0.00107 4,250 4.55 ✓ /api/feed/list 0.00023 12,676 2.92 ✓ /api/search 0.25000 5 1.25 ✓ 注：总贡献 = 单请求成本 × 平均 QPS（如 0.00107 × 4250 ≈ 4.55）\n两种优化方向 高 QPS × 低成本：如 /api/feed/list\n特点：单次请求成本低，但调用量大，积少成多 优化思路：客户端缓存、请求合并、限流 低 QPS × 高成本：如 /api/search\n特点：单次请求成本高，是\u0026quot;重\u0026quot;接口 优化思路：算法优化、减少数据库查询、增加服务端缓存 方法局限性 1. 线性假设的局限 真实场景中，CPU 消耗可能是非线性的：\n当 QPS 超过阈值时，GC 压力陡增 某些接口有批处理逻辑，边际成本递减 应对：检查残差图，如有明显模式，考虑分段回归或非线性模型。\n2. 共线性问题 如果两个接口的 QPS 高度相关（如总是被一起调用），系数会相互\u0026quot;分摊\u0026quot;，单独解读不准确。\n应对：\n检查 VIF（方差膨胀因子），VIF \u0026gt; 10 需警惕 检查条件数（condition number \u0026gt; 1000 说明共线性严重） 对于轻度共线性，Ridge 已能缓解；重度共线性建议合并分析 # VIF 计算示例 from statsmodels.stats.outliers_influence import variance_inflation_factor vif = pd.DataFrame({ \u0026#39;api\u0026#39;: X.columns, \u0026#39;VIF\u0026#39;: [variance_inflation_factor(X.values, i) for i in range(X.shape[1])] }) print(vif[vif[\u0026#39;VIF\u0026#39;] \u0026gt; 10]) # 显示 VIF \u0026gt; 10 的高共线性特征 注意：如果接口间存在强依赖（A 调用 B），共线性会很严重。此时可考虑：\n将强相关接口合并为一组分析 使用 Elastic Net（L1+L2）自动筛选特征 结合调用链路分析，单独评估 3. 遗漏变量偏差 如果有重要因素未纳入模型（如定时任务、后台 GC），系数估计会有偏。\n应对：检查截距项，如果截距很大（\u0026gt;10%）或为负，说明模型可能遗漏了重要因素。\n模型诊断 必查指标 指标 健康范围 说明 测试集 R² \u0026gt; 0.85 模型泛化能力，低于训练集 R² 是正常的 训练集 R² \u0026gt; 0.9 模型拟合能力 Durbin-Watson 1.5-2.5 残差自相关检验，\u0026lt;1.5 说明正自相关 Condition Number \u0026lt; 1000 共线性检验，\u0026gt;1000 说明严重共线性 截距 \u0026gt; 0 且 \u0026lt; 10% 基础开销合理性 诊断代码 from statsmodels.stats.stattools import durbin_watson # 计算诊断指标 train_residuals = y_train - model.predict(X_train) test_residuals = y_test - model.predict(X_test) cond_number = np.linalg.cond(X_train.values) print(f\u0026#34;Durbin-Watson (训练): {durbin_watson(train_residuals):.2f}\u0026#34;) print(f\u0026#34;Durbin-Watson (测试): {durbin_watson(test_residuals):.2f}\u0026#34;) print(f\u0026#34;Condition Number: {cond_number:.0f}\u0026#34;) print(f\u0026#34;截距: {model.intercept_:.2f}%\u0026#34;) 异常处理 测试集 R² 远低于训练集：过拟合，增大正则化参数或减少特征 DW \u0026lt; 1.5：残差有正自相关，增大 Bootstrap 块大小 截距为负：模型存在问题，检查数据质量或遗漏变量 系数大面积为负：共线性严重，考虑特征筛选或使用 Elastic Net 总结 这种方法的优势：\n成本低：利用现有监控数据，无需额外埋点 可持续：可自动化运行，持续追踪 可解释：系数有明确的业务含义 适用场景：\n服务接口数量多（\u0026gt;20 个） 有稳定的监控数据（\u0026gt;500 个采样点） 需要量化各接口的资源消耗占比 接口间依赖关系不太复杂 不适用场景：\n接口之间有强依赖关系（共线性严重） CPU 消耗主要来自异步任务/定时任务 服务处于非稳态（如大促期间流量模式剧变） 完整代码示例 import pandas as pd import numpy as np from sklearn.linear_model import RidgeCV, Ridge from sklearn.model_selection import train_test_split, TimeSeriesSplit from statsmodels.stats.stattools import durbin_watson # 注意：需要先定义 Step 4 中的 block_bootstrap_ci 函数 def analyze_api_cpu_contribution(X, y, test_size=0.2, bootstrap_iterations=100): \u0026#34;\u0026#34;\u0026#34; 分析各 API 接口的 CPU 贡献 参数: X: 特征矩阵，每列是一个接口的 QPS (DataFrame) y: CPU 使用率 (Series) test_size: 测试集比例 bootstrap_iterations: Bootstrap 迭代次数 返回: results: 包含系数、置信区间、贡献的 DataFrame \u0026#34;\u0026#34;\u0026#34; # 1. 数据预处理 X = X.fillna(0) y = y.fillna(y.mean()) non_zero_ratio = (X \u0026gt; 0).mean() valid_cols = X.columns[non_zero_ratio \u0026gt; 0.1] X = X[valid_cols] # 2. 计算平均 QPS（在划分数据集之前，使用全量数据） avg_qps = X.mean() # 3. 划分数据集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=test_size, shuffle=False ) # 4. 训练模型（使用 TimeSeriesSplit 保持时序） model = RidgeCV( alphas=[0.001, 0.01, 0.1, 1.0, 10.0], cv=TimeSeriesSplit(n_splits=5) ) model.fit(X_train, y_train) # 5. 计算置信区间 lower, upper = block_bootstrap_ci( X_train, y_train, model.alpha_, n_iterations=bootstrap_iterations ) # 6. 整理结果 results = pd.DataFrame({ \u0026#39;api\u0026#39;: X.columns, \u0026#39;coefficient\u0026#39;: model.coef_, \u0026#39;ci_lower\u0026#39;: lower, \u0026#39;ci_upper\u0026#39;: upper, \u0026#39;avg_qps\u0026#39;: avg_qps.values, \u0026#39;contribution\u0026#39;: [max(0, c) * q for c, q in zip(model.coef_, avg_qps)], \u0026#39;significant\u0026#39;: lower \u0026gt; 0 }) results = results.sort_values(\u0026#39;contribution\u0026#39;, ascending=False) # 7. 诊断信息 train_residuals = y_train - model.predict(X_train) test_residuals = y_test - model.predict(X_test) print(f\u0026#34;训练集 R²: {model.score(X_train, y_train):.4f}\u0026#34;) print(f\u0026#34;测试集 R²: {model.score(X_test, y_test):.4f}\u0026#34;) print(f\u0026#34;Durbin-Watson (训练): {durbin_watson(train_residuals):.2f}\u0026#34;) print(f\u0026#34;Durbin-Watson (测试): {durbin_watson(test_residuals):.2f}\u0026#34;) print(f\u0026#34;Condition Number: {np.linalg.cond(X_train.values):.0f}\u0026#34;) print(f\u0026#34;截距: {model.intercept_:.2f}%\u0026#34;) return results 参考资料 Ridge Regression - scikit-learn Block Bootstrap for Time Series Variance Inflation Factor Non-negative Least Squares - scipy 本文方法已在生产环境验证，模型 R² 达到 94%-99%，成功定位多个高消耗接口并指导优化。\n","permalink":"https://fancive.github.io/posts/quantify-api-cpu-consumption-with-regression/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e在微服务架构中，一个服务通常包含数十甚至上百个 API 接口。当我们需要优化 CPU 资源消耗时，往往面临一个问题：\u003cstrong\u003e哪些接口消耗了最多的 CPU？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e传统方法是通过 profiling 工具（如 pprof）分析，但这种方法：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e只能看到函数级别的消耗，难以直接关联到接口\u003c/li\u003e\n\u003cli\u003e需要在生产环境开启采样，有一定性能开销\u003c/li\u003e\n\u003cli\u003e难以持续监控和量化\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e本文介绍一种\u003cstrong\u003e基于线性回归\u003c/strong\u003e的方法，利用现有的监控数据（QPS 和 CPU 使用率），估算每个接口的 CPU 贡献。\u003c/p\u003e","title":"如何量化 API 接口的 CPU 资源消耗：一种基于回归分析的方法"},{"content":"引言 在现代软件开发中，部署效率直接影响团队的整体生产力。本文分享了我们团队从手动部署到全自动化部署的实践经验，通过自动化工具将部署时间从 15 分钟缩短到 5 分钟，并完全消除了人工操作带来的错误。\n传统部署流程的痛点 在开发自动化部署工具之前，我们的部署流程需要频繁的人工介入和等待。让我们看看一个典型的部署过程：\n1. 评审与合并阶段 开发人员提交代码后，首先要在代码平台等待其他同学评审并给出 +2。这个过程完全依赖人工，评审人可能正在开会或者忙于其他事情，导致评审时间不可控。\n2. 流水线等待阶段 代码合入 feature 分支后，需要等待大约 5 分钟完成流水线检查，包括编译和各类代码检查。这期间开发人员需要时不时刷新页面查看流水线状态。\n3. 分支合并阶段 流水线通过后，还需要手动将 feature 分支合入部署分支（如沙盒环境分支）。合入后又要再等待一轮流水线，又是 5 分钟左右的等待和检查时间。\n4. 部署准备阶段 接下来是最容易出错的环节：手动处理构建产物 URL。由于只有 master 分支支持构建版本号，开发人员需要手动复制编译产出的 URL，并根据部署需求修改 URL 后缀。这个步骤既繁琐又容易出错。\n5. 部署配置阶段 在部署平台上，需要手动选择正确的服务、环境和发布策略，然后填写前面准备好的编译产出 URL。每一个选项都不能出错，否则可能导致部署失败或者部署到错误的环境。\n6. 部署执行阶段 部署开始后，还需要持续关注部署状态，在特定阶段点击\u0026quot;继续\u0026quot;按钮。整个过程需要人工监控，无法安心去做其他事情。\n问题总结 这个传统的部署流程存在几个明显的问题：\n问题 影响 ⏱️ 时间成本高 整个过程至少 15 分钟，其中大部分是等待时间 👨‍💻 人工介入频繁 需要在不同阶段反复检查状态、手动操作 ⚠️ 容易出错 手动处理 URL 和选择参数容易失误 🔀 体验割裂 需要在多个平台之间切换 自动化解决方案 针对这些痛点，我们设计并实现了一套自动化部署工具，实现了以下核心功能：\n核心功能 1. 自动状态跟踪 工具会自动监控评审状态、流水线状态，无需人工刷新查看。通过轮询机制实时获取各个阶段的进度。\n2. 智能分支管理 评审通过后自动完成分支合并，包括：\n自动检测评审状态（+2） 自动合并到目标分支 自动触发流水线构建 3. 自动参数处理 构建 URL 的获取和修改全部自动化：\n自动提取编译产物 URL 智能修改 URL 后缀适配不同环境 自动填充部署平台参数 4. 一站式操作 整合多平台功能，在一个工具内完成全流程：\n代码评审平台集成 CI/CD 流水线集成 部署平台集成 5. 可视化进度 清晰展示每个阶段的状态：\n✅ 代码评审通过 ⏳ 流水线构建中... (预计 3 分钟) ⏹ 等待部署 技术实现要点 架构设计 用户触发 → 状态机管理 → API 调用 → 进度展示 ↓ 错误处理 → 通知告警 关键技术 状态机模式：管理复杂的部署流程状态转换 异步任务：后台执行耗时操作，不阻塞用户 重试机制：网络异常时自动重试 日志记录：完整记录每次部署的详细信息 收益与效果 通过自动化改造，我们实现了显著的效率提升：\n指标 优化前 优化后 提升 部署时间 15 分钟 5 分钟 66% ↓ 人工操作 6 次 1 次 83% ↓ 错误率 ~5% 0% 100% ↓ 平台切换 3 个 1 个 66% ↓ 其他收益 ✅ 降低学习成本：标准化流程，新人无需学习复杂的部署步骤 ✅ 提升体验：开发人员可以专注于编码，无需关注部署细节 ✅ 减少错误：完全消除手动操作导致的失误 ✅ 可追溯性：完整的部署日志，便于问题排查\n总结与展望 这个工具很好地诠释了\u0026quot;工欲善其事，必先利其器\u0026quot;的理念。通过消除重复的人工操作，不仅提升了团队的开发效率，也让部署过程变得更加可靠和轻松。\n经验总结 自动化要从痛点出发：关注最耗时、最容易出错的环节 渐进式优化：先实现核心功能，再逐步完善 用户体验优先：简单易用比功能全面更重要 可观测性：充分的日志和状态展示帮助排查问题 未来规划 🔄 支持更多部署策略（灰度、蓝绿部署等） 📊 添加部署数据统计和分析 🔔 智能告警和异常自动恢复 🤖 集成 ChatOps，支持聊天工具触发部署 通过持续优化，我们希望让部署成为一个完全自动化、安全可靠的过程，让开发人员能够更专注于业务价值的创造。\n","permalink":"https://fancive.github.io/posts/automating-deployment-pipeline-from-code-review-to-production/","summary":"\u003ch2 id=\"引言\"\u003e引言\u003c/h2\u003e\n\u003cp\u003e在现代软件开发中，部署效率直接影响团队的整体生产力。本文分享了我们团队从手动部署到全自动化部署的实践经验，通过自动化工具将部署时间从 15 分钟缩短到 5 分钟，并完全消除了人工操作带来的错误。\u003c/p\u003e","title":"从代码评审到生产部署：自动化流水线实践"},{"content":" 我让大模型读取了我的2024年每个月的月报(从周报和日记中总结)，得到了以下的生命之轮得分\n生命之轮 2024 基于对全年12个月的完整回顾分析，我现在重新为生命之轮的八个维度进行评分：\n事业发展：8分 (↑1) 优点：\n全年完成多个重要项目：ES监控系统、自动部署工具、圈子项目等 技术能力持续提升，涉及多个领域（Flutter、Docker、Node.js等） 论文研究有进展，完成开题答辩 下半年在工作中展现出更强的影响力和决策能力 待改进：\n工作压力管理仍需优化 项目时间管理需要提升 人际关系：7.5分 (↑0.5) 优点：\n团队协作能力提升，能够处理复杂的跨部门协作 在工作中学会更好的沟通方式 能够在矛盾中寻找共同点 下半年展现出更强的沟通和影响力 待改进：\n在压力下的沟通方式仍需改善 家庭关系的调解技巧需要提升 教育学习：8.5分 (↑0.5) 优点：\n持续的技术学习和算法训练 广泛的知识积累：技术、心理、生产力等多个领域 建立了固定的学习习惯（自习室） 论文研究有实质性进展 积极学习新技术和工具 待改进：\n娱乐时间管理需要优化 学习计划执行的连续性需要提升 心灵成长：8.5分 (↑0.5) 优点：\n持续的冥想练习和自我觉察 情绪管理能力显著提升 能够在压力下保持平和 建立了有效的压力管理机制 年底开始更注重生活的平衡 待改进：\n在家庭矛盾中的情绪管理仍需提升 健康状况：6.5分 (↓0.5) 优点：\n尝试建立运动习惯（健身、瑜伽） 开始注意饮食健康 有意识地关注身体状况 待改进：\n作息极不规律，经常熬夜 运动频率不稳定，年底明显下降 反复出现身体不适（扁桃体、咽喉等） 家庭关系：7.5分 (↓0.5) 优点：\n保持固定的亲子活动和陪伴时间 积极改善育儿方式 重视家庭活动和出游 关注家人健康 待改进：\n家庭矛盾（特别是婆媳关系）需要更好的处理 夫妻相处时间和质量需要提升 育儿方式仍需改进 金钱财富：7分 (↑1) 优点：\n完成重要的房贷决策 开始关注财富积累和管理 有意识地学习财务知识 工作收入稳定，有额外收入来源 待改进：\n需要更系统的财务规划 投资和理财方面需要加强 休闲娱乐：6分 (持平) 优点：\n有固定的家庭出游活动 尝试瑜伽等新的运动方式 有阅读和学习的乐趣 待改进：\n电子娱乐时间严重过度（视频、游戏） 休闲方式单一 作息不规律严重影响休闲质量 总体评估 相比之前的评分，多个维度有所提升，特别是事业发展（+1）、教育学习（+0.5）、心灵成长（+0.5）和金钱财富（+1）。这反映出你在工作能力、学习成长和财务管理方面都有明显进步。\n然而，健康状况（-0.5）和家庭关系（-0.5）出现了下滑，主要是由于作息不规律、运动习惯不稳定，以及家庭矛盾的持续存在。\n建议重点关注：\n作息管理：这是最急需改善的问题，影响到多个维度的表现 运动习惯：重建稳定的运动频率，避免三天打鱼两天晒网 家庭关系：需要更多耐心和智慧来处理家庭矛盾 时间管理：特别是控制电子娱乐时间，这严重影响了其他维度的发展 生活平衡：在追求事业和学习进步的同时，不要忽视健康和家庭 ","permalink":"https://fancive.github.io/posts/wheel-of-life-2024/","summary":"\u003cblockquote\u003e\n\u003cp\u003e我让大模型读取了我的2024年每个月的月报(从周报和日记中总结)，得到了以下的生命之轮得分\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"生命之轮-2024\"\u003e生命之轮 2024\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"wheel_of_life_2024\" loading=\"lazy\" src=\"/images/img/wheel_of_life_2024.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e基于对全年12个月的完整回顾分析，我现在重新为生命之轮的八个维度进行评分：\u003c/p\u003e","title":"Wheel of Life 2024"},{"content":"在当今复杂的IT环境中，快速准确地诊断和解决系统问题变得越来越具有挑战性。本文将探讨如何设计一个由大语言模型（LLM）驱动的诊断引擎，作为自动定位系统的核心组件，以智能化方式处理系统告警。\n引言 随着系统规模和复杂性的增加，传统的基于规则的告警诊断方法往往力不从心。我们需要一种更智能、更灵活的方法来分析和诊断系统告警。这就是LLM驱动的诊断引擎发挥作用的地方。\n宏观系统架构：自动定位系统 在深入探讨LLM驱动的诊断引擎之前，让我们先了解它在整个自动定位系统中的位置。自动定位系统是一个复杂的生态系统，旨在自动检测、诊断和解决IT环境中的问题。以下是系统的宏观架构：\ngraph TB A[监控系统] --\u0026gt;|告警| B[告警聚合器] B --\u0026gt;|结构化告警| C[LLM驱动的诊断引擎] D[日志系统] --\u0026gt;|相关日志| C E[配置管理数据库] --\u0026gt;|系统配置| C F[知识库] --\u0026gt;|历史案例| C C --\u0026gt;|诊断结果| G[自动修复系统] C --\u0026gt;|诊断报告| H[运维仪表板] I[人工反馈] --\u0026gt;|优化信息| C 以Elasticsearch作为数据源为例:\nsequenceDiagram participant ES as Elasticsearch participant AR as 告警规则执行器 participant AM as 告警管理器 participant LLM as LLM诊断引擎 participant NT as 通知系统 loop 定期执行 AR-\u0026gt;\u0026gt;ES: 执行查询表达式 ES--\u0026gt;\u0026gt;AR: 返回查询结果 AR-\u0026gt;\u0026gt;AR: 应用时间窗口和阈值 alt 触发告警条件 AR-\u0026gt;\u0026gt;AM: 生成告警 AM-\u0026gt;\u0026gt;LLM: 请求诊断 LLM-\u0026gt;\u0026gt;LLM: 分析告警上下文 LLM--\u0026gt;\u0026gt;AM: 返回诊断结果 AM-\u0026gt;\u0026gt;NT: 发送通知 end end 在这个宏观架构中，LLM驱动的诊断引擎扮演着核心角色：\n数据汇聚点：它接收来自监控系统的结构化告警、日志系统的相关日志、配置管理数据库的系统配置信息，以及知识库中的历史案例。\n智能分析中心：利用这些输入，诊断引擎进行深度分析，识别问题的根本原因。\n决策支持：它为自动修复系统提供精确的诊断结果，指导后续的修复操作。\n信息展示：通过运维仪表板，它为运维团队提供直观的诊断报告。\n持续学习：通过处理人工反馈，诊断引擎不断优化其性能。\n现在，让我们深入了解LLM驱动的诊断引擎的内部架构和工作原理。\nLLM驱动的诊断引擎架构 我们的LLM驱动的诊断引擎架构包括以下核心组件：\n上下文收集器 提示生成器 LLM处理器 后处理器 诊断报告生成器 反馈处理器 让我们通过一个图表来直观地了解这些组件是如何协同工作的：\ngraph TD A[告警输入] --\u0026gt;|结构化数据| B[上下文收集器] B --\u0026gt;|丰富的上下文| C[提示生成器] D[(知识库)] --\u0026gt;|相关知识| C C --\u0026gt;|定制提示| E[LLM处理器] E --\u0026gt;|原始输出| F[后处理器] F --\u0026gt;|结构化诊断| G[诊断报告生成器] H[反馈处理器] --\u0026gt;|学习更新| D H --\u0026gt;|优化| C 详细组件设计 1. 上下文收集器 上下文收集器的主要任务是收集与告警相关的所有必要信息。这包括系统状态、相关日志、历史告警等。\nasync def collect_context(alert_data): context = { \u0026#34;alert\u0026#34;: alert_data, \u0026#34;system_state\u0026#34;: await get_system_state(), \u0026#34;related_logs\u0026#34;: await fetch_related_logs(alert_data), \u0026#34;historical_alerts\u0026#34;: await get_historical_alerts(alert_data[\u0026#39;query\u0026#39;]), \u0026#34;current_config\u0026#34;: await fetch_current_config() } return context 2. 提示生成器 提示生成器负责创建针对特定告警的定制LLM提示。它使用模板系统并集成知识库中的相关信息。\ndef generate_prompt(context, knowledge): template = \u0026#34;\u0026#34;\u0026#34; 分析以下告警情况： {alert_description} 系统当前状态： {system_state} 相关日志： {related_logs} 历史告警情况： {historical_alerts} 当前配置： {current_config} 相关知识： {relevant_knowledge} 请提供： 1. 可能的根本原因分析 2. 潜在的系统影响 3. 建议的调查步骤 4. 可能的解决方案 \u0026#34;\u0026#34;\u0026#34; return template.format( alert_description=context[\u0026#39;alert\u0026#39;], system_state=context[\u0026#39;system_state\u0026#39;], related_logs=summarize_logs(context[\u0026#39;related_logs\u0026#39;]), historical_alerts=summarize_historical_alerts(context[\u0026#39;historical_alerts\u0026#39;]), current_config=context[\u0026#39;current_config\u0026#39;], relevant_knowledge=knowledge ) 3. LLM处理器 LLM处理器管理与大语言模型的交互，处理重试、超时和错误。\nasync def process_with_llm(prompt): try: response = await llm_client.generate(prompt, max_tokens=1000) return response.choices[0].text except Exception as e: logger.error(f\u0026#34;LLM处理错误: {e}\u0026#34;) return None 4. 后处理器 后处理器解析和结构化LLM的输出，应用规则基础的验证和增强。\ndef postprocess_llm_output(raw_output): sections = extract_sections(raw_output) enhanced_sections = apply_enhancement_rules(sections) return { \u0026#34;root_cause\u0026#34;: enhanced_sections.get(\u0026#34;根本原因分析\u0026#34;), \u0026#34;impact\u0026#34;: enhanced_sections.get(\u0026#34;潜在的系统影响\u0026#34;), \u0026#34;investigation_steps\u0026#34;: enhanced_sections.get(\u0026#34;建议的调查步骤\u0026#34;), \u0026#34;solutions\u0026#34;: enhanced_sections.get(\u0026#34;可能的解决方案\u0026#34;), \u0026#34;confidence_score\u0026#34;: calculate_confidence_score(enhanced_sections) } 5. 诊断报告生成器 诊断报告生成器创建结构化和易读的诊断报告，支持多种输出格式。\ndef generate_diagnostic_report(processed_diagnosis, context): report = { \u0026#34;alert_summary\u0026#34;: summarize_alert(context[\u0026#39;alert\u0026#39;]), \u0026#34;diagnosis\u0026#34;: processed_diagnosis, \u0026#34;visualizations\u0026#34;: generate_visualizations(context, processed_diagnosis), \u0026#34;recommendations\u0026#34;: prioritize_recommendations(processed_diagnosis[\u0026#39;solutions\u0026#39;]), \u0026#34;metadata\u0026#34;: { \u0026#34;generated_at\u0026#34;: datetime.now().isoformat(), \u0026#34;model_version\u0026#34;: llm_client.model_version, \u0026#34;confidence_score\u0026#34;: processed_diagnosis[\u0026#39;confidence_score\u0026#39;] } } return render_report_template(report) 6. 反馈处理器 反馈处理器收集用户反馈，更新知识库并优化提示模板。\nasync def process_feedback(diagnosis_id, feedback): diagnosis = await fetch_diagnosis(diagnosis_id) await update_knowledge_base(diagnosis, feedback) await optimize_prompt_template(diagnosis[\u0026#39;prompt\u0026#39;], feedback) await update_model_performance_metrics(feedback) 集成与优化 为了提高系统的性能和可扩展性，我们采用了以下策略：\n使用异步编程提高并发处理能力 实现缓存机制，避免重复处理相似的告警 使用流处理架构（如Apache Kafka）来处理大规模告警 实现金丝雀发布和A/B测试，以安全地推出新的诊断策略 持续改进 为了确保诊断引擎能够不断提高其性能，我们实施了以下措施：\n建立反馈循环，不断优化LLM提示和后处理规则 定期评审诊断性能，识别改进机会 考虑集成多个LLM模型，根据不同类型的告警选择最适合的模型 与自动定位系统的集成 LLM驱动的诊断引擎通过以下方式与自动定位系统的其他组件集成：\n告警接收：通过标准化的API接口从告警聚合器接收结构化告警。\n更丰富的上下文：\n从日志系统拉取相关的日志条目 从配置管理数据库获取最新的系统配置信息 从知识库检索相关的历史案例 诊断结果输出：\n向自动修复系统提供结构化的诊断结果，包括建议的修复步骤 将格式化的诊断报告推送到运维仪表板 反馈处理：\n接收来自运维团队的反馈 利用反馈信息优化诊断模型和知识库 总结 核心要点 系统架构设计\n诊断引擎作为自动定位系统的核心组件 六大核心组件协同工作：上下文收集、提示生成、LLM处理、后处理、报告生成、反馈循环 与监控、日志、配置、知识库等系统深度集成 智能诊断能力\n利用 LLM 强大的语言理解能力分析复杂告警 自动关联多源数据（日志、配置、历史案例） 提供根因分析、影响评估和解决方案建议 持续学习机制\n通过反馈循环不断优化诊断准确性 知识库动态更新，积累运维经验 提示模板持续优化，提升 LLM 输出质量 工程化实践\n异步架构提升并发处理能力 缓存机制避免重复计算 流处理架构应对大规模告警 金丝雀发布保障系统稳定性 实施建议 分阶段实施：先从高频、模式化的告警场景入手，逐步扩展到复杂场景 人机协作：初期保持人工审核，逐步提升自动化程度 质量评估：建立诊断准确率、召回率等指标体系 成本控制：合理使用缓存、批处理等技术降低 LLM API 调用成本 未来展望 多模态诊断：结合日志、监控图表、配置变更等多模态数据 预测性运维：从被动诊断走向主动预测 自动修复：从诊断建议到自动执行修复操作 知识图谱：构建系统组件关系图谱，提升诊断准确性 参考资源 LangChain 文档 - LLM 应用开发框架 Elasticsearch 告警系统 AIOps 最佳实践 通过 LLM 驱动的智能诊断引擎，我们能够显著提升运维效率，降低 MTTR（平均修复时间），让运维团队从繁琐的告警处理工作中解放出来，专注于更有价值的系统优化和创新工作。\n","permalink":"https://fancive.github.io/posts/llm-driven-alert-diagnosis-engine-design/","summary":"\u003cp\u003e在当今复杂的IT环境中，快速准确地诊断和解决系统问题变得越来越具有挑战性。本文将探讨如何设计一个由大语言模型（LLM）驱动的诊断引擎，作为自动定位系统的核心组件，以智能化方式处理系统告警。\u003c/p\u003e","title":"Llm Driven Alert Diagnosis Engine Design(设计新一代LLM驱动的告警诊断引擎)"},{"content":"first pass 5-10 mins\ntitle, abstract, and introduction section and sub-section headings conclusions references question Category: What type of paper is this? A measurement paper? An analysis of an existing system? A description of a research prototype?\nIt’s a survey paper on the methods and trend on log based AIOps.\nIt does not seem to focus on a specific system or prototype, but rather explore the current state of research in the field.\nContext: Which other papers is it related to? Which theoretical bases were used to analyze the problem?\nS. He, J. Zhu, P. He and M. R. Lyu, \u0026ldquo;Experience Report: System Log Analysis for Anomaly Detection,\u0026rdquo; 2016 IEEE 27th International Symposium on Software Reliability Engineering (ISSRE), Ottawa, ON, Canada, 2016, pp. 207-218, doi: 10.1109/ISSRE.2016.21. R. B. Yadav, P. S. Kumar and S. V. Dhavale, \u0026ldquo;A Survey on Log Anomaly Detection using Deep Learning,\u0026rdquo; 2020 8th International Conference on Reliability, Infocom Technologies and Optimization (Trends and Future Directions) (ICRITO), Noida, India, 2020, pp. 1215-1220, doi: 10.1109/ICRITO48877.2020.9197818. R. Vinayakumar, K. P. Soman and P. Poornachandran, \u0026ldquo;Long short-term memory based operation log anomaly detection,\u0026rdquo; 2017 International Conference on Advances in Computing, Communications and Informatics (ICACCI), Udupi, India, 2017, pp. 236-242, doi: 10.1109/ICACCI.2017.8125846. The paper explores the use of SVM, NLP and decision tree for log analysis and discusses several data anylysis techniques, such as clustering and time series analysis.\nCorrectness: Do the assumptions appear to be valid?\nThe paper conducted a systematic review to investigate the motheds and trend in log research for AIOps. The author found that the use of log data is essential for AIOps and machine learning techniques are widely used in log research.\nBased on the survey , the assumptions appear to be valid.\nContributions: What are the paper’s main contributions?\nThe paper presents a comprehencive survey of the research on log analysis in the field of AIOps.\nThe paper reviews various techniques and algorithms used for log analysis in AIOps, including statistical analysis, machine learning, NLP and graph analysis. The paper discusses the application of log analysis of AIOps, including fault detection and diagnosis, anomaly detection, root cause analysis and predictive maintenance. The paper indentify the key chanllenges in log analysis for AIOps, including the vast amount of data, the need of real-time analysis and the need to integrate multiple data sources. Clarity: Is the paper well written?\nIt apears to be well-organized and follows a logical stucture. It starts by providing background information on AIOps and log management, followed by a review on related work in the field.The paper then descibes the methodology used to conduct the survey and presents the key findings. The discussion section analyzes the results. The conclusion summarizes the main contributions of the study and outlines future direction of study.\nsecond pass 1 hour\nﬁgures(插图), diagrams(示意图、流程图) and other illustrations mark relevant unread references question summarize the main thrust\nThe main thrust of the survey is to identify the challenges and opportunities in using AI for log analysis, and to understand the current state of research in this area.\nThe survey identifies several trends in the use of AI for log analysis. One is the use of unsupervised learning. Another is the use of deep learning.\nThere are still many challenges to be addressed, such as the lack of labelled data, the complexity of log data and the needs of industrial deployment.\nsome evidence\nthird pass 1 / 4-5 hour\nre-implement the paper: that is, making the same assumptions as the authors, re-create the work\n","permalink":"https://fancive.github.io/posts/paper_reading_a_survey_on_log_research_of_aiops/","summary":"\u003ch1 id=\"first-pass\"\u003efirst pass\u003c/h1\u003e\n\u003cp\u003e5-10 mins\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003etitle, abstract, and introduction\u003c/li\u003e\n\u003cli\u003esection and sub-section headings\u003c/li\u003e\n\u003cli\u003econclusions\u003c/li\u003e\n\u003cli\u003ereferences\u003c/li\u003e\n\u003c/ol\u003e","title":"[Paper reading] A Survey On Log Research Of AIOps: Methods and Trends"},{"content":"first pass 5-10 mins\ntitle, abstract, and introduction section and sub-section headings conclusions references question Category: What type of paper is this? A measurement paper? An analysis of an existing system? A description of a research prototype?\nIt is a descriptive paper that discusses the challenges and innovations related to the implementation of AIOps in real-world IT operations.\nContext: Which other papers is it related to? Which theoretical bases were used to analyze the problem?\nP. Huang, C. Guo et al., \u0026ldquo;Capturing and Enhancing In Situ System Observability for Failure Detection\u0026rdquo;, Proceedings of OSDI, 2018.\nY. Xu, K. Sui et al., \u0026ldquo;Improving Service Availability of Cloud Systems by Predicting Disk Error\u0026rdquo;, Proceedings of USNIX ATC, 2018.\nQ. Lin, K. Hsieh et al., \u0026ldquo;Predicting Node Failure in Cloud Service Systems\u0026rdquo;, proceedings of FSE, 2018.\nSome of the theoretical frameworks that are relevant to AIOps include supervised and unsupervised learning, reinforcement learning, deep learning, and natural language processing.\nCorrectness: Do the assumptions appear to be valid?\nContributions: What are the paper’s main contributions?\ntalking about the motivation and importance of AIOps; describing the real-world challenges of building AIOps solutions based on the experience in Microsoft introducing a set of sample AIOps solutions that have successfully benefited Microsoft service products sharing some learnings from AIOps practice. Clarity: Is the paper well written?\nit appears to be well-organized, it introduces the the vision, challenge and innovation of AIOps.\nsecond pass 1 hour\nﬁgures(插图), diagrams(示意图、流程图) and other illustrations mark relevant unread references question summarize the main thrust \u0026amp; some evidence\nthe vision of AIOps\nHigh service intelligence. predict its future status(quality, cost, workload) High customer satisfaction. understand customer usage behavior and take proactive actions(configuration, quality issue) High engineering productivity. developers are relieved of tedious tasks(investigating, fixing repeated issues) challenges of building AIOps\nmethodologies and mindset(different from the traditional engineering mindset, not ai solves all) Engineering changes (improvement of data quality and quantity) building ML models (no clear ground truth labels, complex dependencies, huge manual efforts) innovations on AIOps\nCross-disciplinary research Close collaboration between academia and industry third pass 1 / 4-5 hour\nre-implement the paper: that is, making the same assumptions as the authors, re-create the work\n","permalink":"https://fancive.github.io/posts/paper_reading_aiops_real-world_challenges_and_research_innovations/","summary":"\u003ch2 id=\"first-pass\"\u003efirst pass\u003c/h2\u003e\n\u003cp\u003e5-10 mins\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003etitle, abstract, and introduction\u003c/li\u003e\n\u003cli\u003esection and sub-section headings\u003c/li\u003e\n\u003cli\u003econclusions\u003c/li\u003e\n\u003cli\u003ereferences\u003c/li\u003e\n\u003c/ol\u003e","title":"[paper reading]AIOps: Real-World Challenges and Research Innovations"},{"content":"引言 在学习优秀开源项目的源码时，我们不仅能学到具体的技术实现，更能领悟到工程实践中的设计智慧。Filebeat 作为 Elastic Stack 中的日志采集器，其 Harvester 模块的设计就是一个典范。本文通过深入分析 Harvester 的协程管理机制，总结了四个核心问题的解决方案，这些经验可以直接应用到我们日常的 Go 项目开发中。\n核心问题：\n如何为主协程添加超时控制？ 主协程结束时如何优雅关闭其他协程？ 如何控制多个协程的关闭顺序？ 如何从外部安全地关闭任务并等待清理完成？ 总体流程 Filebeat 对每个 path 都会创建一个 harvester，harvester 负责逐行读取文件内容。它的上游是 input，input 负责管理 harvester，它的下游是 output，output 负责消费每一行消息。\n通过阅读 harvester 代码，我学习到了如何管理多个相互依赖的协程的生命周期，具体可以拆分为 4 个问题：\n为主协程添加超时控制协程 主协程结束如何关闭超时控制协程和其他协程 (monitorFileSize) 关闭协程时如何控制不同 job 间的先后执行顺序 如何从外部关闭任务，并等待清理完成 超时控制协程 (h *Harvester)Run() 方法里，会创建超时控制协程，在此协程中会消费 2 个事件：\n超时事件 closeTimeout 主任务结束标志 h.done 一旦接收到其中一个事件，那么会调用 stop 并关闭 reader：\n// Closes reader after timeout or when done channel is closed // This routine is also responsible to properly stop the reader go func(source string) { closeTimeout := make(\u0026lt;-chan time.Time) // starts close_timeout timer if h.config.CloseTimeout \u0026gt; 0 { closeTimeout = time.After(h.config.CloseTimeout) } select { // Applies when timeout is reached case \u0026lt;-closeTimeout: logger.Infof(\u0026#34;Closing harvester because close_timeout was reached: %s\u0026#34;, source) // Required when reader loop returns and reader finished case \u0026lt;-h.done: } h.stop() err := h.reader.Close() if err != nil { logger.Errorf(\u0026#34;Failed to stop harvester for file: %v\u0026#34;, err) } }(h.state.Source) 主协程关闭其他附属功能协程 一句话，主要是通过调用 stop (在 defer 里)，stop 里会 close(h.done)：\nselect { case \u0026lt;-h.done: h.stopWg.Done() return nil default: } defer func() { // Channel to stop internal harvester routines h.stop() // Makes sure file is properly closed when the harvester is stopped h.cleanup() harvesterRunning.Add(-1) // Marks harvester stopping completed h.stopWg.Done() }() 其他协程会消费 h.done：\nRun 函数开头，用于立刻停止 超时控制协程，这里 select 消费了 2 个 chan (其中一个是 h.done) select{ // Applies when timeout is reached case\u0026lt;-closeTimeout: logger.Infof(\u0026#34;Closing harvester because close_timeout was reached: %s\u0026#34;, source) // Required when reader loop returns and reader finished case\u0026lt;-h.done: } Run 函数的 for-loop，这里是主要的逻辑实现地方 for{ select{ case\u0026lt;-h.done: return nil default: } // 读文件 ... } monitorFileSize 监控文件大小协程，需要和主协程一起结束 Run 方法开始的地方，创建其他子协程之前 逐行读文件的 for-loop 里 doneWg waitGroup 用来在主协程 (Run) 发送结束信号 close(h.done) 时等 monitorFileSize 协程关闭。\n2 个问题，这个 wg 用来等什么协程，在哪等：\n用来等的是 monitorFileSize 监控文件大小协程 h.doneWg.Add(1) go func() { h.monitorFileSize() h.doneWg.Done() }() 在 stop 函数里等 (小写 stop) //stop is intended for internal use and closed thedone channel tostop execution func(h *Harvester) stop() { h.stopOnce.Do(func() { close(h.done) // Wait for goroutines monitoring h.done to terminate before closing source. h.doneWg.Wait() filesMetrics.Remove(h.id.String()) }) } 那么 stop 函数在哪被调用呢？\nRun 函数的 defer 操作里 超时控制的协程里 外部调用 Stop 时 这里有个疑问，为什么在 stop 里面要 h.doneWg.Wait()，看上去 monitorFileSize 即使不 wait 也能正常退出，后来再读后面代码发现是为了先执行 monitorFileSize 后执行 cleanup 关闭句柄并改变关闭状态。\n这里可以看出，当主协程因为某种原因结束时，我们希望先等 monitorFileSize 协程先退出后再执行后续操作 cleanup，这里的顺序是：\nh.stop() h.monitorFileSize 协程退出 h.cleanup() stopWg waitGroup - 结束完成 2 个问题，这个 wg 用来等什么协程，在哪等：\n用来等待 Run 函数的 defer 也完成后再退出 (cleanup 函数，改变 Harvester 状态为关闭，并关闭句柄) defer func() { // Channel to stop internal harvester routines h.stop() // Makes sure file is properly closed when the harvester is stopped h.cleanup() harvesterRunning.Add(-1) // Marks harvester stopping completed h.stopWg.Done() }() 在 Stop 函数里等 (大写) //Stop stops harvester and waits for completion func(h *Harvester)Stop() { h.stop() // Prevent stopWg.Wait() to be called at the same time as stopWg.Add(1) h.stopLock.Lock() h.stopWg.Wait() h.stopLock.Unlock() } 可以看出，这个 wg 的用途是当外部关闭该任务时，保证 stop 和 cleanup 被执行完成。\n总结 核心设计模式 通过对 Filebeat Harvester 的分析，我们学习到了优雅的协程生命周期管理模式：\n双 WaitGroup 模式\ndoneWg：用于等待内部协程（如 monitorFileSize）完成 stopWg：用于外部调用者等待整个任务完全清理 这种设计实现了分层的关闭控制 Channel + WaitGroup 组合\ndone channel：作为广播信号，通知所有协程停止 sync.Once：保证关闭操作只执行一次 WaitGroup：确保关闭顺序可控 优雅关闭的顺序控制\n主协程结束 → close(h.done) → 等待 monitorFileSize → cleanup → stopWg.Done() 关键技术要点 技术点 作用 使用场景 done channel 广播停止信号 通知多个协程同时停止 doneWg 等待内部协程 保证清理前内部任务完成 stopWg 等待清理完成 外部调用者等待资源释放 sync.Once 防止重复关闭 多个协程可能触发关闭 stopLock 防止竞态 保护 stopWg 的并发访问 实践建议 设计长生命周期协程时\n提供明确的停止信号机制（如 done channel） 为主协程设计超时控制，避免无限等待 使用 defer 确保清理代码一定执行 管理多个协程时\n使用 WaitGroup 控制关闭顺序 区分内部协程等待和外部调用等待 使用 sync.Once 保证关闭操作幂等性 资源清理时\n先停止业务逻辑（close channel） 再等待子任务完成（doneWg.Wait） 最后清理资源（cleanup） 通知外部调用者（stopWg.Done） 可复用的代码模式 type Worker struct { done chan struct{} doneWg *sync.WaitGroup // 等待内部协程 stopOnce sync.Once stopWg *sync.WaitGroup // 等待清理完成 stopLock sync.Mutex } func (w *Worker) Run() error { w.stopWg.Add(1) defer func() { w.stop() w.cleanup() w.stopWg.Done() }() // 启动监控协程 w.doneWg.Add(1) go func() { w.monitor() w.doneWg.Done() }() // 主逻辑 for { select { case \u0026lt;-w.done: return nil default: // 业务处理 } } } func (w *Worker) stop() { w.stopOnce.Do(func() { close(w.done) w.doneWg.Wait() // 等待内部协程 }) } func (w *Worker) Stop() { w.stop() w.stopLock.Lock() w.stopWg.Wait() // 等待清理完成 w.stopLock.Unlock() } 扩展思考 这种协程管理模式不仅适用于文件采集场景，也可以应用于：\n网络连接池管理 定时任务调度 消息队列消费者 长连接服务（WebSocket、gRPC） 关键是理解分层关闭和顺序控制的设计思想。\n参考 filebeat/input/log/harvester.go\n// Harvester contains all harvester related data type Harvester struct { logger *logp.Logger id uuid.UUID config config source harvester.Source // the source being watched // shutdown handling done chan struct{} doneWg *sync.WaitGroup stopOnce sync.Once stopWg *sync.WaitGroup stopLock sync.Mutex // internal harvester state state file.State states *file.States log *Log // file reader pipeline reader reader.Reader encodingFactory encoding.EncodingFactory encoding encoding.Encoding // event/state publishing outletFactory OutletFactory publishState func(file.State) bool metrics *harvesterProgressMetrics onTerminate func() } ","permalink":"https://fancive.github.io/posts/what_i_learned_from_filebeat_harvester/","summary":"\u003ch2 id=\"引言\"\u003e引言\u003c/h2\u003e\n\u003cp\u003e在学习优秀开源项目的源码时，我们不仅能学到具体的技术实现，更能领悟到工程实践中的设计智慧。Filebeat 作为 Elastic Stack 中的日志采集器，其 Harvester 模块的设计就是一个典范。本文通过深入分析 Harvester 的协程管理机制，总结了四个核心问题的解决方案，这些经验可以直接应用到我们日常的 Go 项目开发中。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e核心问题\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如何为主协程添加超时控制？\u003c/li\u003e\n\u003cli\u003e主协程结束时如何优雅关闭其他协程？\u003c/li\u003e\n\u003cli\u003e如何控制多个协程的关闭顺序？\u003c/li\u003e\n\u003cli\u003e如何从外部安全地关闭任务并等待清理完成？\u003c/li\u003e\n\u003c/ul\u003e","title":"我从filebeat-harvester中学到什么"},{"content":"引言 Filebeat 是 Elastic Stack 中用于收集和转发日志数据的轻量级采集器。通过深入阅读 Filebeat 的源码，我们可以学习到许多优秀的设计模式应用。本文将介绍 Filebeat 中使用的四种核心设计模式：Registry + LazyInit、Observer、Strategy 和 Object Pool，并分析它们如何帮助 Filebeat 实现高性能和良好的可扩展性。\nRegistry 模式与 LazyInit 的结合 设计思想 Registry 模式（注册表模式）允许在运行时动态注册和查找组件，而 LazyInit（延迟初始化）则确保组件只在真正需要时才被创建。Filebeat 将这两种模式结合使用，实现了灵活的插件化架构。\n实现细节 获取工厂函数\n在 filebeat/input.New 中，通过 Registry 获取对应类型的工厂函数：\nf, err = GetFactory(input.config.Type) Registry 查找逻辑\nfunc GetFactory(name string) (Factory, error) { if _, exists := registry[name]; !exists { return nil, fmt.Errorf(\u0026#34;Error creating input. No such input type exist: \u0026#39;%v\u0026#39;\u0026#34;, name) } return registry[name], nil } 组件注册\n各个 input 类型在初始化时自动注册到 registry：\nfunc init() { err := input.Register(\u0026#34;log\u0026#34;, NewInput) if err != nil { panic(err) } } 优势 解耦：配置和具体实现分离，便于扩展新的 input 类型 延迟加载：只有被使用的组件才会被实例化，节省资源 插件化：新增功能只需注册即可，无需修改核心代码 Observer 模式（观察者模式） 设计思想 Observer 模式定义了对象间的一对多依赖关系，当一个对象状态改变时，所有依赖它的对象都会收到通知。Filebeat 使用 Observer 模式实现事件总线（Event Bus），用于组件间的消息传递。\n发布事件 func (b *bus) Publish(e Event) { b.RLock() defer b.RUnlock() logp.Debug(\u0026#34;bus\u0026#34;, \u0026#34;%s: %+v\u0026#34;, b.name, e) // 遍历所有订阅者，发送感兴趣的事件 for _, listener := range b.listeners { if listener.interested(e) { listener.channel \u0026lt;- e } } } 订阅事件 func (b *bus) Subscribe(filter ...string) Listener { listener := \u0026amp;listener{ filter: filter, bus: b, channel: make(chan Event, 100), // 带缓冲的 channel } b.Lock() defer b.Unlock() b.listeners = append(b.listeners, listener) return listener } 应用场景 组件解耦：发布者和订阅者互不依赖 异步处理：通过 channel 实现异步事件传递 灵活订阅：支持基于过滤器的选择性订阅 Strategy 模式（策略模式） 设计思想 Strategy 模式定义了一系列算法，将每个算法封装起来，使它们可以互相替换。Filebeat 在 Kafka 输出中使用 Strategy 模式来支持不同的分区策略。\n实现代码 // 定义策略映射表 var partitioners = map[string]partitionBuilder{ \u0026#34;random\u0026#34;: cfgRandomPartitioner, \u0026#34;round_robin\u0026#34;: cfgRoundRobinPartitioner, \u0026#34;hash\u0026#34;: cfgHashPartitioner, } func initPartitionStrategy(config *Config) (Partitioner, error) { name := config.Partition // 根据配置选择策略 mk := partitioners[name] if mk == nil { return nil, fmt.Errorf(\u0026#34;unknown kafka partition mode %v\u0026#34;, name) } // 构造具体的分区器 partitioner, err := mk(config) // ... } 优势 算法可替换：通过配置切换不同的分区策略 易于扩展：新增策略只需添加到映射表 代码清晰：每种策略独立实现，职责单一 Object Pool 模式（对象池模式） 设计思想 Object Pool 模式通过复用对象来减少频繁创建和销毁对象的开销。在高并发场景下，这种模式能显著提升性能并减少 GC 压力。\n实现代码 定义对象池\nvar ackChanPool = sync.Pool{ New: func() interface{} { return \u0026amp;ackChan{ ch: make(chan batchAckMsg, 1), } }, } 从池中获取对象\nfunc newACKChan(seq uint, start, count int, states []clientState) *ackChan { ch := ackChanPool.Get().(*ackChan) ch.next = nil ch.seq = seq ch.start = start ch.count = count ch.states = states return ch } 归还对象到池\nfunc releaseACKChan(c *ackChan) { c.next = nil // 清理引用，防止内存泄漏 ackChanPool.Put(c) } 性能优势 减少 GC 压力：复用对象减少垃圾回收次数 提升性能：避免频繁的内存分配和初始化 适用场景：高频创建销毁的短生命周期对象 总结 通过分析 Filebeat 的源码，我们学习到了四种经典设计模式的实际应用：\nRegistry + LazyInit：实现插件化架构和延迟加载 Observer：解耦组件，实现灵活的事件驱动 Strategy：封装算法变化，支持策略切换 Object Pool：优化性能，减少资源开销 这些设计模式的合理运用，使 Filebeat 具备了良好的可扩展性、可维护性和高性能。在我们日常的 Go 项目开发中，也可以借鉴这些优秀的工程实践。\n参考资源 Filebeat 官方文档 Filebeat GitHub 仓库 ","permalink":"https://fancive.github.io/posts/design_pattern_used_in_filebeat/","summary":"\u003ch2 id=\"引言\"\u003e引言\u003c/h2\u003e\n\u003cp\u003eFilebeat 是 Elastic Stack 中用于收集和转发日志数据的轻量级采集器。通过深入阅读 Filebeat 的源码，我们可以学习到许多优秀的设计模式应用。本文将介绍 Filebeat 中使用的四种核心设计模式：Registry + LazyInit、Observer、Strategy 和 Object Pool，并分析它们如何帮助 Filebeat 实现高性能和良好的可扩展性。\u003c/p\u003e","title":"Filebeat 中使用的设计模式"},{"content":"引言 在 Go 语言中，string 和 []byte 之间的转换是非常常见的操作。但你是否注意到，当将一个 3 字节的字符串转为 []byte 时，得到的切片容量却是 32？这背后隐藏着 Go 运行时的内存分配策略。本文将通过汇编代码分析，深入探究这个有趣的现象，帮助你理解 Go 在底层是如何处理类型转换和内存分配的。\n核心问题：\n为什么 []byte(\u0026quot;abc\u0026quot;) 的容量是 32 而不是 3？ string 转 []byte 的底层实现是什么？ Go 运行时如何进行内存分配？ 问题发现 最近在 code review 时发现，将 string 转 []byte 时，得到的 []byte 的容量会发生变化。比如下面这段代码的输出是 32：\nfunc main() { s1 := \u0026#34;abc\u0026#34; fmt.Println(cap([]byte(s1))) // 输出：32 } 为什么长度为 3 的字符串转换后，容量却是 32？\n汇编代码分析 要理解这个问题，我们需要深入到汇编层面。首先编译代码并查看汇编输出：\n-N 禁止优化 -S 输出汇编代码 -l 禁止内联\ngo build -gcflags=\u0026#34;-N -l -S\u0026#34; go.go 得到以下代码\n\u0026#34;\u0026#34;.main STEXT size=293 args=0x0 locals=0xc0 0x0000 00000 (/home/xxx/go.go:5)\tTEXT\t\u0026#34;\u0026#34;.main(SB), ABIInternal, $192-0 0x0000 00000 (/home/xxx/go.go:5)\tMOVQ\t(TLS), CX 0x0009 00009 (/home/xxx/go.go:5)\tLEAQ\t-64(SP), AX 0x000e 00014 (/home/xxx/go.go:5)\tCMPQ\tAX, 16(CX) 0x0012 00018 (/home/xxx/go.go:5)\tJLS\t283 0x0018 00024 (/home/xxx/go.go:5)\tSUBQ\t$192, SP 0x001f 00031 (/home/xxx/go.go:5)\tMOVQ\tBP, 184(SP) 0x0027 00039 (/home/xxx/go.go:5)\tLEAQ\t184(SP), BP 0x002f 00047 (/home/xxx/go.go:5)\tFUNCDATA\t$0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB) 0x002f 00047 (/home/xxx/go.go:5)\tFUNCDATA\t$1, gclocals·4dc9e0f0c3406fbbbbd2ec99068e8282(SB) 0x002f 00047 (/home/xxx/go.go:5)\tFUNCDATA\t$2, gclocals·8dcadbff7c52509cfe2d26e4d7d24689(SB) 0x002f 00047 (/home/xxx/go.go:5)\tFUNCDATA\t$3, \u0026#34;\u0026#34;.main.stkobj(SB) 0x002f 00047 (/home/xxx/go.go:6)\tPCDATA\t$0, $1 0x002f 00047 (/home/xxx/go.go:6)\tPCDATA\t$1, $0 0x002f 00047 (/home/xxx/go.go:6)\tLEAQ\tgo.string.\u0026#34;abc\u0026#34;(SB), AX 0x0036 00054 (/home/xxx/go.go:6)\tMOVQ\tAX, \u0026#34;\u0026#34;.s1+104(SP) 0x003b 00059 (/home/xxx/go.go:6)\tMOVQ\t$3, \u0026#34;\u0026#34;.s1+112(SP) 0x0044 00068 (/home/xxx/go.go:7)\tPCDATA\t$0, $2 0x0044 00068 (/home/xxx/go.go:7)\tLEAQ\t\u0026#34;\u0026#34;..autotmp_4+56(SP), CX 0x0049 00073 (/home/xxx/go.go:7)\tPCDATA\t$0, $1 0x0049 00073 (/home/xxx/go.go:7)\tMOVQ\tCX, (SP) 0x004d 00077 (/home/xxx/go.go:7)\tPCDATA\t$0, $0 0x004d 00077 (/home/xxx/go.go:7)\tMOVQ\tAX, 8(SP) 0x0052 00082 (/home/xxx/go.go:7)\tMOVQ\t$3, 16(SP) 0x005b 00091 (/home/xxx/go.go:7)\tCALL\truntime.stringtoslicebyte(SB) 0x0060 00096 (/home/xxx/go.go:7)\tMOVQ\t40(SP), AX 0x0065 00101 (/home/xxx/go.go:7)\tMOVQ\t32(SP), CX 0x006a 00106 (/home/xxx/go.go:7)\tPCDATA\t$0, $3 0x006a 00106 (/home/xxx/go.go:7)\tMOVQ\t24(SP), DX 0x006f 00111 (/home/xxx/go.go:7)\tPCDATA\t$0, $0 0x006f 00111 (/home/xxx/go.go:7)\tMOVQ\tDX, \u0026#34;\u0026#34;..autotmp_2+160(SP) 0x0077 00119 (/home/xxx/go.go:7)\tMOVQ\tCX, \u0026#34;\u0026#34;..autotmp_2+168(SP) 0x007f 00127 (/home/xxx/go.go:7)\tMOVQ\tAX, \u0026#34;\u0026#34;..autotmp_2+176(SP) 0x0087 00135 (/home/xxx/go.go:7)\tMOVQ\tAX, \u0026#34;\u0026#34;..autotmp_3+48(SP) 0x008c 00140 (/home/xxx/go.go:7)\tMOVQ\tAX, (SP) 0x0090 00144 (/home/xxx/go.go:7)\tCALL\truntime.convT64(SB) 0x0095 00149 (/home/xxx/go.go:7)\tPCDATA\t$0, $1 0x0095 00149 (/home/xxx/go.go:7)\tMOVQ\t8(SP), AX 0x009a 00154 (/home/xxx/go.go:7)\tPCDATA\t$0, $0 0x009a 00154 (/home/xxx/go.go:7)\tPCDATA\t$1, $1 0x009a 00154 (/home/xxx/go.go:7)\tMOVQ\tAX, \u0026#34;\u0026#34;..autotmp_5+96(SP) 0x009f 00159 (/home/xxx/go.go:7)\tPCDATA\t$1, $2 0x009f 00159 (/home/xxx/go.go:7)\tXORPS\tX0, X0 0x00a2 00162 (/home/xxx/go.go:7)\tMOVUPS\tX0, \u0026#34;\u0026#34;..autotmp_1+120(SP) 0x00a7 00167 (/home/xxx/go.go:7)\tPCDATA\t$0, $1 0x00a7 00167 (/home/xxx/go.go:7)\tPCDATA\t$1, $1 0x00a7 00167 (/home/xxx/go.go:7)\tLEAQ\t\u0026#34;\u0026#34;..autotmp_1+120(SP), AX 0x00ac 00172 (/home/xxx/go.go:7)\tMOVQ\tAX, \u0026#34;\u0026#34;..autotmp_7+88(SP) 0x00b1 00177 (/home/xxx/go.go:7)\tTESTB\tAL, (AX) 0x00b3 00179 (/home/xxx/go.go:7)\tPCDATA\t$0, $2 0x00b3 00179 (/home/xxx/go.go:7)\tPCDATA\t$1, $0 0x00b3 00179 (/home/xxx/go.go:7)\tMOVQ\t\u0026#34;\u0026#34;..autotmp_5+96(SP), CX 0x00b8 00184 (/home/xxx/go.go:7)\tPCDATA\t$0, $4 0x00b8 00184 (/home/xxx/go.go:7)\tLEAQ\ttype.int(SB), DX 0x00bf 00191 (/home/xxx/go.go:7)\tPCDATA\t$0, $2 0x00bf 00191 (/home/xxx/go.go:7)\tMOVQ\tDX, \u0026#34;\u0026#34;..autotmp_1+120(SP) 0x00c4 00196 (/home/xxx/go.go:7)\tPCDATA\t$0, $1 0x00c4 00196 (/home/xxx/go.go:7)\tMOVQ\tCX, \u0026#34;\u0026#34;..autotmp_1+128(SP) 0x00cc 00204 (/home/xxx/go.go:7)\tTESTB\tAL, (AX) 0x00ce 00206 (/home/xxx/go.go:7)\tJMP\t208 0x00d0 00208 (/home/xxx/go.go:7)\tMOVQ\tAX, \u0026#34;\u0026#34;..autotmp_6+136(SP) 0x00d8 00216 (/home/xxx/go.go:7)\tMOVQ\t$1, \u0026#34;\u0026#34;..autotmp_6+144(SP) 0x00e4 00228 (/home/xxx/go.go:7)\tMOVQ\t$1, \u0026#34;\u0026#34;..autotmp_6+152(SP) 0x00f0 00240 (/home/xxx/go.go:7)\tPCDATA\t$0, $0 0x00f0 00240 (/home/xxx/go.go:7)\tMOVQ\tAX, (SP) 0x00f4 00244 (/home/xxx/go.go:7)\tMOVQ\t$1, 8(SP) 0x00fd 00253 (/home/xxx/go.go:7)\tMOVQ\t$1, 16(SP) 0x0106 00262 (/home/xxx/go.go:7)\tCALL\tfmt.Println(SB) 0x010b 00267 (/home/xxx/go.go:9)\tMOVQ\t184(SP), BP 0x0113 00275 (/home/xxx/go.go:9)\tADDQ\t$192, SP 0x011a 00282 (/home/xxx/go.go:9)\tRET 0x011b 00283 (/home/xxx/go.go:9)\tNOP 0x011b 00283 (/home/xxx/go.go:5)\tPCDATA\t$1, $-1 0x011b 00283 (/home/xxx/go.go:5)\tPCDATA\t$0, $-1 0x011b 00283 (/home/xxx/go.go:5)\tCALL\truntime.morestack_noctxt(SB) 0x0120 00288 (/home/xxx/go.go:5)\tJMP\t0 声明部分\n\u0026#34;\u0026#34;.main STEXT size=293 args=0x0 locals=0xc0 0x0000 00000 (/home/xxx/go.go:5)\tTEXT\t\u0026#34;\u0026#34;.main(SB), ABIInternal, $192-0 0x0000 00000 (/home/xxx/go.go:5)\tMOVQ\t(TLS), CX 0x0009 00009 (/home/xxx/go.go:5)\tLEAQ\t-64(SP), AX 0x000e 00014 (/home/xxx/go.go:5)\tCMPQ\tAX, 16(CX) 0x0012 00018 (/home/xxx/go.go:5)\tJLS\t283 0x0018 00024 (/home/xxx/go.go:5)\tSUBQ\t$192, SP 0x001f 00031 (/home/xxx/go.go:5)\tMOVQ\tBP, 184(SP) 0x0027 00039 (/home/xxx/go.go:5)\tLEAQ\t184(SP), BP main函数声明\n0x0000 00000 (/home/xxx/go.go:3)\tTEXT\t\u0026#34;\u0026#34;.main(SB), ABIInternal, $192-0 “”.main(被链接之后名字会变成main.main) 是一个全局的函数符号，存储在.text` 段中，该函数的地址是相对于整个地址空间起始位置的一个固定的偏移量。 $192-0 它分配了 192 字节的栈帧，且不接收参数，不返回值。$192-0 中的 192 代表局部变量字节数总和，-0 代表在 192 的地址基础上空出0的长度作为传入和返回对象, 即没有参数和返回值 0x0000 00000 (/home/xxx/go.go:3)\tMOVQ\t(TLS), CX TLS 是一个由 runtime 维护的虚拟寄存器，保存了指向当前 g 的指针，这个 g 的数据结构会跟踪 goroutine 运行时的所有状态值 将当前 *g 保存到 CX 0x0009 00009 (/home/xxx/go.go:3)\tCMPQ\tSP, 16(CX) 看一看 runtime 源代码中对于 g 的定义:\ntype g struct { stack stack // 16 bytes // stackguard0 is the stack pointer compared in the Go stack growth prologue. // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. stackguard0 uintptr stackguard1 uintptr 我们可以看到 16(CX) 对应的是 g.stackguard0，是 runtime 维护的一个阈值，该值会被拿来与栈指针(stack-pointer)进行比较以判断一个 goroutine 是否马上要用完当前的栈空间。\n0x0012 00018 (/home/xxx/go.go:5)\tJLS\t283 jumps to 283 if SP \u0026lt;= g.stackguard0 0x0018 00024 (/home/xxx/go.go:5)\tSUBQ\t$192, SP main 作为调用者，通过对虚拟栈指针(stack-pointer)寄存器做减法，将其栈帧大小增加了 192 个字节(回忆一下栈是向低地址方向增长，所以这里的 SUBQ 指令是将栈帧的大小调整得更大了)。\n0x001f 00031 (/home/xxx/go.go:5)\tMOVQ\tBP, 184(SP) 8 个字节(183(SP)-192(SP)) 用来存储当前帧指针 BP (这是一个实际存在的寄存器)的值，以支持栈的展开和方便调试\n0x0027 00039 (/home/xxx/go.go:5)\tLEAQ\t184(SP), BP 跟着栈的增长，LEAQ 指令计算出帧指针的新地址，并将其存储到 BP 寄存器中。\n看下字符串处理部分 0x002f 00047 (/Users/fancivez/tmp/go.go:6)\tLEAQ\tgo.string.\u0026#34;abc\u0026#34;(SB), AX 0x0036 00054 (/Users/fancivez/tmp/go.go:6)\tMOVQ\tAX, \u0026#34;\u0026#34;.s1+104(SP) 0x003b 00059 (/Users/fancivez/tmp/go.go:6)\tMOVQ\t$3, \u0026#34;\u0026#34;.s1+112(SP) 0x0044 00068 (/Users/fancivez/tmp/go.go:7)\tPCDATA\t$0, $2 0x0044 00068 (/Users/fancivez/tmp/go.go:7)\tLEAQ\t\u0026#34;\u0026#34;..autotmp_4+56(SP), CX 0x0049 00073 (/Users/fancivez/tmp/go.go:7)\tPCDATA\t$0, $1 0x0049 00073 (/Users/fancivez/tmp/go.go:7)\tMOVQ\tCX, (SP) 0x004d 00077 (/Users/fancivez/tmp/go.go:7)\tPCDATA\t$0, $0 0x004d 00077 (/Users/fancivez/tmp/go.go:7)\tMOVQ\tAX, 8(SP) 0x0052 00082 (/Users/fancivez/tmp/go.go:7)\tMOVQ\t$3, 16(SP) 0x005b 00091 (/Users/fancivez/tmp/go.go:7)\tCALL\truntime.stringtoslicebyte(SB) 0x002f 00047 (/Users/fancivez/tmp/go.go:6)\tLEAQ\tgo.string.\u0026#34;abc\u0026#34;(SB), AX 取字面值的地址,(字面值的数据在.data区域分配)\n0x0036 00054 (/Users/fancivez/tmp/go.go:6)\tMOVQ\tAX, \u0026#34;\u0026#34;.s1+104(SP) 将数据地址移动到栈指针104字节位置\n0x003b 00059 (/Users/fancivez/tmp/go.go:6)\tMOVQ\t$3, \u0026#34;\u0026#34;.s1+112(SP) 把字符串长度(3)移动到112字节位置\n0x005b 00091 (/Users/fancivez/tmp/go.go:7)\tCALL\truntime.stringtoslicebyte(SB) 这个函数调用就是关键！runtime.stringtoslicebyte 负责执行 string 到 []byte 的转换，它会分配内存并复制数据。\n内存分配策略 Go 运行时的内存分配器采用了预分配策略来优化性能。当分配小对象时，不会精确分配所需的字节数，而是按照预定义的 size class 进行分配。\nSize Class 机制 Go 运行时定义了多个内存块大小等级（size class），常见的包括：\n8 bytes 16 bytes 32 bytes 48 bytes 64 bytes \u0026hellip; 当我们需要分配 3 字节时，运行时会选择最接近且大于等于 3 的 size class，也就是 32 bytes。这就解释了为什么容量是 32 而不是 3。\n为什么这样设计？ 减少内存碎片：统一的 size class 可以更好地复用内存块 提升分配效率：预定义的大小等级简化了内存管理逻辑 降低开销：避免为每个微小的分配请求都进行精确的内存管理 总结 核心发现 string 转 []byte 会发生内存分配和数据复制\n通过 runtime.stringtoslicebyte 函数实现 不是简单的类型转换，涉及实际的内存操作 容量由内存分配器的 size class 决定\n小对象按预定义的 size class 分配 3 字节的需求会分配到 32 字节的 size class 这是性能优化的结果，不是 bug 汇编代码揭示了底层实现\n字符串字面值存储在 .data 段 栈帧分配遵循特定的布局规则 函数调用约定包括参数传递和寄存器使用 性能影响 这个发现对实际编程有重要意义：\n场景 建议 频繁转换 尽量避免重复的 string ↔ []byte 转换 大字符串 考虑使用 unsafe 包进行零拷贝转换（需注意安全性） 性能敏感 复用 []byte 切片，避免频繁分配 小字符串 容量\u0026quot;浪费\u0026quot;可以接受，因为分配器本来就会这样做 实践建议 理解转换成本\n// 低效：每次循环都分配新内存 for _, s := range strings { b := []byte(s) process(b) } // 高效：复用缓冲区 buf := make([]byte, 0, 1024) for _, s := range strings { buf = append(buf[:0], s...) process(buf) } 零拷贝转换（仅在确保安全的情况下使用）\nimport \u0026#34;unsafe\u0026#34; // 危险！仅当你确定不会修改返回的 []byte 时使用 func stringToByteSliceUnsafe(s string) []byte { return *(*[]byte)(unsafe.Pointer(\u0026amp;s)) } 选择合适的类型\n如果数据不需要修改，保持 string 类型 如果需要频繁修改，使用 []byte 避免两者之间的频繁转换 扩展阅读 通过本文的分析方法，你也可以探究其他 Go 语言特性的底层实现：\ninterface 的内存布局 map 的扩容机制 channel 的通信原理 goroutine 的调度过程 掌握汇编级别的理解能力，可以帮助你：\n写出更高性能的代码 深入理解语言特性 定位复杂的性能问题 做出更明智的技术决策 参考资源 Go and Plan9 Assembly A Quick Guide to Go\u0026rsquo;s Assembler Go Assembly by Example Go 汇编入门知识 Go 内存分配器设计 ","permalink":"https://fancive.github.io/posts/go_string_byte/","summary":"\u003ch2 id=\"引言\"\u003e引言\u003c/h2\u003e\n\u003cp\u003e在 Go 语言中，string 和 []byte 之间的转换是非常常见的操作。但你是否注意到，当将一个 3 字节的字符串转为 []byte 时，得到的切片容量却是 32？这背后隐藏着 Go 运行时的内存分配策略。本文将通过汇编代码分析，深入探究这个有趣的现象，帮助你理解 Go 在底层是如何处理类型转换和内存分配的。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e核心问题\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e为什么 \u003ccode\u003e[]byte(\u0026quot;abc\u0026quot;)\u003c/code\u003e 的容量是 32 而不是 3？\u003c/li\u003e\n\u003cli\u003estring 转 []byte 的底层实现是什么？\u003c/li\u003e\n\u003cli\u003eGo 运行时如何进行内存分配？\u003c/li\u003e\n\u003c/ul\u003e","title":"深入理解 Go String 转 []byte 的容量分配机制"},{"content":"引言 Interface 是 Go 语言最强大的特性之一，但同时也是最容易让人困惑的部分。本文翻译自 Ian Lance Taylor 的博客文章，深入探讨了 Go interface 的内部实现机制，帮助你理解 interface 的静态类型、动态类型以及值拷贝的工作原理。\n核心要点：\nInterface 同时具有静态类型和动态类型 Interface 的值拷贝行为 Interface 的内部数据结构（itable） 指针接收者与值接收者的区别 原文链接：https://www.airs.com/blog/archives/281\nInterface 的类型系统 虽然 Go 中 interface 的使用是灵活的，但它们也有令人困惑的方面。\n接口值例如—-一个interface类型的变量—-包含其他类型的值。 interface类型被称为静态类型，因为这是编译器在编译期看到的类型。 另一种类型只在运行时可见，称为动态类型(dynamic type)。 根据定义，动态类型可以是任何除了interface的其他类型。\n当通过赋值或函数调用复制一个 interface 值时，你是在复制动态类型里的值。 这通常是大多数类型通常的工作方式。 但是，使用 interface 的一个非常常见的情况是将动态类型作为指针使用。 在这种情况下，当你拷贝接口时，你拷贝的其实是指针，但是你不会拷贝指针指向的值。 在许多情况下，如果方法要更改值本身, 类型(type)的方法需要使用指针作为接收方(receiver)。 当 interface 必需的方法发生这种情况时，interface 实际上要求动态类型是指针类型。\n这意味着，技术上来说虽然 interface 总是作为一个值拷贝，但在实际使用中，它们的行为常常好像是通过引用拷贝的。 也就是说，尽管没有显式标记，接口对象通常充当指针的角色。 这可能会让你困惑，直到你明白到底发生了什么。\n在我上一篇文章中提到，对于 gccgo，一个 interface 总是包含一个指向值的指针。 现在我要纠正这个错误: 如果一个程序存储了包含了指针(或者unsafe.Pointer类型) 的 interface，那么存储在 interface 中的值就是指针本身。 也就是说，gccgo 不存储指向指针的指针(这需要分配堆空间来保存指针)。 这是一种自然的高效方法，因为实际上大多数接口对象都包含指针。\n这种高效的方法贯穿于方法的实现。 方法总是接受指针作为接收方(receiver)参数。 如果方法的接收方(receiver)类型实际上不是指针，那么指针将隐式解引用，并在方法代码的开头复制值。 这意味着当在接口上调用方法时，存储在接口中的值可以直接传递给方法，而不管动态类型是否是指针。 (和上一篇文章一样，gc 编译器做的事情有些不同。)\n补充 原文其实有点绕\n如果 v 是一个 interface{}\n什么是interface?\nInterface 同时是两种东西\n一些方法的集合 它自身也是一种类型 interface{} type, the empty interface is the interface that has no methods.\n由于没有 implements 关键字，所有类型都至少实现了empty interface， 这意味着，如果你编写一个将 interface{}作为参数的函数，则可以为该函数传任何值作为参数。\nfunc DoSomething(v interface{}) { // ... } 令人困惑的地方在于\n初学者倾向于相信“ v 是任何类型的” ，但这是错误的。 v 不是任何类型的; 它是 interface{}类型的。\n当向 DoSomething 函数传递一个值时，Go 运行时将执行类型转换(如果必要) ，并将该值转换为interface{}值。 所有值在运行时只有一种类型，而 v的静态类型是interface{}。\nruss的说法\ntype Stringer interface { String() string } 接口值表示为包含两个单词的一对(单词)，\n指向存储在接口中的类型信息的指针 指向相关数据的指针。 将 b 分配给 Stringer 类型的接口值会同时设置接口值的两个单词。\n接口值中的第一个单词指向我所称的interface table或者 itable\nItable 以一些关于所涉及的类型的元数据开始，然后加上一个函数指针列表。 注意，itable 对应于 interface type，而不是动态类型。在我们的示例中,\nStringer 持有类型Binary的itable 列出了用于满足Stringer的方法，而后者只是“ String”：Binary的其他方法（“ Get”）未在“ itable” 中出现。\nthe itable for Stringer holding type Binary lists the methods used to satisfy Stringer, which is just String: Binary’s other methods (Get) make no appearance in the itable.\n接口值中的第二个单词指向实际数据，在这种情况下为b的副本。 赋值var s Stringer = b 产生b的副本，而不是指向b的指针，原因与var c uint64 = b产生副本的原因相同：如果b以后发生变化，则s和c应该具有原始值，而不是新值。 存储在接口中的值可能任意大，但是只有一个字专用于在接口结构中保存该值，因此该分配在堆上分配了一块内存，并将指针记录在一个字槽中。\nThe second word in the interface value points at the actual data, in this case a copy of b. The assignment var s Stringer = b makes a copy of b rather than point at b for the same reason that var c uint64 = b makes a copy: if b later changes, s and c are supposed to have the original value, not the new one. Values stored in interfaces might be arbitrarily large, but only one word is dedicated to holding the value in the interface structure, so the assignment allocates a chunk of memory on the heap and records the pointer in the one-word slot.\n总结 通过本文的学习，我们深入理解了 Go interface 的以下核心概念：\n关键要点 双重类型系统\n静态类型：编译期确定的 interface 类型 动态类型：运行时实际存储的值的类型 值拷贝机制\nInterface 赋值时拷贝的是动态类型的值 当动态类型是指针时，拷贝的是指针本身 这使得 interface 既可以表现为值语义，也可以表现为引用语义 内部数据结构\nInterface 值由两个字（word）组成 第一个字：指向 itable 的指针（包含类型信息和方法表） 第二个字：指向实际数据的指针 性能优化\ngccgo 避免存储指向指针的指针 方法调用时的高效传递机制 itable 复用相同的 interface 类型和动态类型组合 实践建议 理解 interface{} 不是\u0026quot;任意类型\u0026quot;，而是一个具体的 interface 类型 注意指针接收者和值接收者对 interface 实现的影响 合理使用 interface 来实现解耦和多态 理解 interface 赋值的拷贝行为，避免意外修改 扩展阅读 Go Data Structures: Interfaces - Russ Cox How to use interfaces in Go Why can\u0026rsquo;t I assign a struct to an interface? What\u0026rsquo;s the meaning of interface? ","permalink":"https://fancive.github.io/posts/go_interface/","summary":"\u003ch2 id=\"引言\"\u003e引言\u003c/h2\u003e\n\u003cp\u003eInterface 是 Go 语言最强大的特性之一，但同时也是最容易让人困惑的部分。本文翻译自 Ian Lance Taylor 的博客文章，深入探讨了 Go interface 的内部实现机制，帮助你理解 interface 的静态类型、动态类型以及值拷贝的工作原理。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e核心要点\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eInterface 同时具有静态类型和动态类型\u003c/li\u003e\n\u003cli\u003eInterface 的值拷贝行为\u003c/li\u003e\n\u003cli\u003eInterface 的内部数据结构（itable）\u003c/li\u003e\n\u003cli\u003e指针接收者与值接收者的区别\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e原文链接：https://www.airs.com/blog/archives/281\u003c/p\u003e\n\u003c/blockquote\u003e","title":"[翻译] Go Interface 深度解析"},{"content":" github.com/fancive ","permalink":"https://fancive.github.io/contact/","summary":"\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/fancive\"\u003egithub.com/fancive\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"Contact"}]