跬步 On Coding Hugo https://zhu327.github.io/ zh-cn Sat, 07 Mar 2026 12:00:00 +0800 复盘:ClawOps AI Agent https://zhu327.github.io/2026/03/07/%E5%A4%8D%E7%9B%98clawops-ai-agent/ Sat, 07 Mar 2026 12:00:00 +0800 https://zhu327.github.io/2026/03/07/%E5%A4%8D%E7%9B%98clawops-ai-agent/ <h4 id="前言">前言</h4> <p>过去的两周,我经历了一次充满激情的开发,也经历了一次彻底的失败。</p> <p>起因是我准备在内部做一个公共的“运维AI机器人”。我满怀热情地规划了多租户架构、复杂的并发控制、MCP(Model Context Protocol)对接以及各种内部系统的打通。然而,经过两周的爆肝,项目上线没多久就被平台团队勒令下线,而从业务价值来看,老板其实也并不需要这样一个东西。</p> <p>俗话说,失败是成功之母。虽然项目黄了,但这两周踩过的坑、写过的废代码,都是技术架构演进路上的宝贵经验。我在这里复盘一下整个过程,聊聊我的技术实现、妥协,以及如果让我重新设计,我会怎么做。</p> <p></p> <h3 id="1-雄心勃勃的初始架构">1. 雄心勃勃的初始架构</h3> <p>项目初期的定位非常庞大:一个服务于全公司的公共运维Agent平台。为了支撑这个目标,我设计了一套相当复杂的服务端架构。</p> <p>项目本身基于 <a href="https://github.com/sipeed/picoclaw">PicoClaw</a>。</p> <p>在这个阶段,我的思维主要停留在<strong>“如何用技术解决所有场景的问题”</strong>。</p> <ul> <li><strong>多租户与隔离</strong>:每个用户拥有独立的Workspace,利用文件系统工具和Shell工具的黑名单机制进行隔离。</li> <li><strong>高并发的AgentLoop池</strong>:为了支持多用户,我设计了一个AgentLoop Worker池。当Inbound Bus收到消息时,自动分配Worker处理;处理完毕后通过Outbound消息池并发调用Channel的发送接口,随后Worker自动回收。</li> <li><strong>Web Channel的设计</strong>:为了让用户能在网页上使用,我做了一个Web Channel。前端通过POST接口发送消息到Inbound Bus,服务端通过SSE(Server-Sent Events)向前端持续推送AI的回复。同时,Tool调用的过程也会产生摘要,通过Hook机制经SSE推送给前端,连会话的Title都是AI自动生成并推送的。</li> <li><strong>状态与存储</strong>:Web会话和消息数据全部落盘MySQL。因为是公共服务,Inbound进入前和Outbound发出后,都需要对接内部认证组件拦截并识别用户,将用户信息注入Message Metadata,最终融合到LLM的System Prompt中。</li> <li><strong>内部IM(WPS协作)集成</strong>:通过WebSocket连接获取内部IM机器人的消息,提取出内部用户信息注入Inbound,再通过平台的发消息API回传给用户。由于共享Metadata,WPS端和Web端可以共用同一个AgentLoop实例和Workspace记忆。</li> </ul> <p>初期架构流转图大概是这样的:</p> <p><img src="https://github.com/user-attachments/assets/b2a7c58f-0a49-4a92-a9fe-ce11bf989f73" alt="Image3" width="700px" /></p> <p>除此之外,我还实现了一个<strong>多用户的Cron任务管理服务</strong>。系统会遍历每个用户的Workspace,拉起Cron实例。AgentLoop中会记录用户的 <code>last channel</code>,以此决定定时任务的执行结果该推送到Web还是WPS(由于Web没有主动推送能力,还需要做特殊处理屏蔽Web Channel记录)。</p> <h3 id="2-核心能力建设与-妥协">2. 核心能力建设与“妥协”</h3> <p>在Agent本体骨架搭好后,下一步就是Skill(技能)的建设。这里我遇到了几个典型的架构决策问题。</p> <h4 id="wps-365-mcp的对接与proxy模式">WPS 365 MCP的对接与Proxy模式</h4> <p>我们内部使用了WPS 365,它提供了MCP。但是它的MCP使用OAuth2协议,AccessToken只有2小时有效期。 如何让AI顺畅调用?我最初的想法是写个Wrapper程序直连数据库刷新Token,但这太不优雅了。最终的妥协方案是:<strong>在Web服务上暴露一个WPS MCP Proxy</strong>。</p> <p>调用这个Proxy时携带User ID,Proxy负责去数据库换Token、组装认证请求头,最后转发给WPS。这样一来,AI只需要用普通的 <a href="https://github.com/f/mcptools"><code>mcptools</code></a> 配合用户ID就能完成调用。</p> <h4 id="cmdb对接-cli-vs-mcp">CMDB对接:CLI vs MCP</h4> <p>在对接内部CMDB平台时,我有了一个洞察:<strong>其实MCP并没有那么神圣,面向Agent开发的CLI工具才是未来</strong>。因为Agent的Shell Tool结合Skill可以搞定一切CLI的调用,而且极为灵活。</p> <p>于是我把内部服务直接包成了一个CLI工具给Agent调用,并写了Skill说明文档。但Leader认为,这种能力应该包装成标准的MCP提供给其他生态。</p> <p>这是一个典型的业务视角与开发视角的冲突。没问题,我把它改成了MCP暴露,但紧接着认证又成了痛点——现阶段做AI工具调用,搞复杂的统一认证太累了,其实最简单的固定Token透传才是效能最高的。</p> <h3 id="3-失败的根因与挣扎">3. 失败的根因与挣扎</h3> <p>项目开发完了,写好了帮助文档,准备大干一场。然后,就迎来了平台团队的审核。</p> <p><strong>下线。</strong></p> <p>复盘这个结果,核心死穴在于:<strong>Sandbox(沙盒)隔离方案太简陋</strong>。</p> <p>我原本以为,在程序层面做一个Shell命令的黑名单(比如禁用 <code>rm</code>、<code>ssh</code>)就万事大吉了。但我忽略了平台本身网络环境的复杂性。内部平台机器之间的网络是没有严格隔离的。Agent最核心的能力是能够<strong>自主编写Skill扩展能力、执行脚本</strong>。这意味着无论我怎么在应用层做黑名单,只要Agent能执行Python/Shell,它就能在内网里横冲直撞。要想解决,必须依赖底层平台做深度的容器网络隔离,而这在短时间内根本推不动。</p> <p>服务端走不通,我不甘心,于是决定<strong>转向客户端(桌面版)</strong>。</p> <p>我用Golang的 <code>Wails</code> 框架,花了极短的时间糊了一个和网页端一样的桌面客户端。</p> <ul> <li>Wails甚至能直接Hook之前写的Gin路由。</li> <li>因为不支持SSE,我把推送机制改造成了Wails自带的Event Bus。</li> <li>MySQL太重?直接切成SQLite本地存储。</li> <li>WPS协作只能单实例WebSocket连接?我在服务端保留了一个非常轻量的“消息转发网关”,根据User ID把消息路由到对应打开的客户端上。</li> </ul> <p>技术上走通了,但这次碰到了最终的BOSS——<strong>业务价值伪需求</strong>。 从老板的角度来看,团队并不需要一个新的、复杂的公共Agent平台。能力强的同事早就自己部署了类似的开源项目(比如OpenClaw),而能力稍弱的同事用Cursor就能解决80%的代码和运维问题。</p> <h3 id="4-复盘-如果重来-我会怎么设计">4. 复盘:如果重来,我会怎么设计?</h3> <p>在之前的文章中我提到过,解决问题有不同的思维层次。</p> <p>我这次的失败,在于我用<strong>第一层(用技术解决看到的所有问题)</strong>的精力,去挑战了<strong>第四层(从组织与平台底座出发看演进)</strong>的难题,最终因为基础设施不匹配和伪需求而夭折。</p> <p>如果让我重新规划这个项目,我绝对不会立项做一个“多租户的企业级公共Agent”。我会将其<strong>彻底定位为一个极致轻量化的“个人AI助手(Personal AI Agent)”</strong>。</p> <p>以下是我的重构思路:</p> <h4 id="1-抛弃统一认证-拥抱本地配置-identity">1. 抛弃统一认证,拥抱本地配置(Identity)</h4> <p>既然是个人助手,就不要去对接什么内部复杂的SSO认证了。用户的身份定义完全下放。</p> <p>用户只需要在本地修改自己的 <code>USER.md</code> 文件,明确自己的职责、系统偏好等。对于CMDB这种系统,直接在本地配置里写入私人Token,LLM在调用CLI或MCP时自动拼装读取即可。</p> <h4 id="2-wps-365-mcp-授权的本地化改造">2. WPS 365 MCP 授权的本地化改造</h4> <p>不需要服务端的Proxy和数据库。我会开发一个专用的CLI工具。</p> <p>用户在本地执行CLI生成认证链接,浏览器授权后将Callback的AccessToken及其Refresh Token直接存入本地配置目录(如 <code>~/.wps/config.json</code>)。之后的Token刷新完全由本地CLI自动完成,Agent直接调用本地工具即可。</p> <h4 id="3-极简的channel消息路由">3. 极简的Channel消息路由</h4> <p>保留服务端的“消息路由网关”,但不做任何用户认证。</p> <p>WPS机器人在收到消息后,直接告诉用户他当前的 <code>Chat ID</code>。用户只需在自己本地助手的配置里填入这个 <code>Chat ID</code>。本地助手连上服务端网关的WebSocket,网关只负责无脑地按 <code>Chat ID</code> 转发消息。安全、轻量、解耦。</p> <p>新的架构图将变得异常清晰且易于维护:</p> <p><img src="https://github.com/user-attachments/assets/a3867f4b-3fdf-4616-a69c-0404cde65ad7" alt="Image3" width="700px" /></p> <h3 id="5-结语">5. 结语</h3> <p>软件开发的过程,本质上是对业务需求进行抽象、提炼并构建逻辑的过程。</p> <p>这次历时两周的“失败”,虽然没有产出能在公司内部大放异彩的平台产品,但它逼着我理清了Agent在ToB复杂网络下的安全死结,也帮我认清了“大而全的平台”与“小而美的个人工具”之间的边界。</p> <p>做技术不能一味地追求架构的宏大(什么都想上多租户、Worker池、分布式),最终还是要服务于真实的痛点。现在的个人版架构,虽然看起来像是一个妥协的产物,但它没有了沉重的安全包袱,没有了复杂的数据库依赖,反而能把Agent的核心能力(工具调用、记忆、任务流)发挥到极致。</p> <p>有时,砍掉一半的需求,反而能得到一个更健壮的架构。这就是我这两周交出的学费。</p> AI工程落地实践 https://zhu327.github.io/2026/02/11/ai%E5%B7%A5%E7%A8%8B%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5/ Wed, 11 Feb 2026 14:00:00 +0800 https://zhu327.github.io/2026/02/11/ai%E5%B7%A5%E7%A8%8B%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5/ <p>前几天在整理项目代码的时候,看着 commit log 里那些由 AI 生成的代码,突然有点感慨。以前写代码是体力和脑力的双重劳动,现在好像变成了单纯的脑力博弈——跟 AI 的博弈。</p> <p>这篇想絮絮叨叨聊一下从传统的开发模式转变为 Vibe Coding 的过程中,我踩过的坑以及现在摸索出来的一套还算顺手的 SOP。</p> <h4 id="1-回顾-前ai时代的手工作坊">1. 回顾:前AI时代的手工作坊</h4> <p>在那时候,我接一个大需求,流程基本是雷打不动的:</p> <ol> <li><strong>拆解</strong>:把大需求掰碎了,变成一个个小任务。</li> <li><strong>设计</strong>:看老代码,定方案。哪里要改,哪里要加,脑子里或者纸上得有个谱。</li> <li><strong>搬砖</strong>:按计划写代码,写单测。</li> <li><strong>验证</strong>:跑通端对端流程。</li> <li><strong>Code Review</strong>:提 PR,等同事挑刺,改代码,合并。</li> </ol> <p>这个过程里,最重要的一步其实往往被忽视,就是<strong>复盘</strong>。这波开发里学到了什么新模式?引入了什么新坑?怎么避免下次再犯?这种“复利”思维才是工程师成长的关键。</p> <p></p> <h4 id="2-vibe-coding-时代的冲击与适应">2. Vibe Coding 时代的冲击与适应</h4> <p>进入 AI 时代(或者说 Cursor 时代)后,我一开始也是懵的,试错了很多次,现在的 SOP 变成了这样:</p> <ol> <li><strong>聊需求</strong>:先不急着写代码。拉着 ChatGPT 或者 Gemini 开聊。讨论业界怎么做,现在的项目痛点在哪。这一步是为了定方向,最后产出一份靠谱的需求文档。</li> <li><strong>定计划(<code>feat.md</code>大法)</strong>: <ul> <li>我会在 Cursor 里建个 <code>feat.md</code>,把需求文档丢进去。</li> <li>在 Cursor 里 <code>@feat.md</code>,让 AI 出 Plan。</li> <li><strong>重点</strong>:如果 Plan 太大,我会让它拆。如果 Plan 漏了东西,只要不致命,我先不打断,记在小本本上,等它写完代码再补。</li> <li>有时候 Plan 实在离谱,改动量太大,我会回头改 <code>feat.md</code>,限制它的发挥范围,只做第一阶段。</li> </ul></li> <li><strong>生成</strong>:Plan 只要逻辑通顺,我就让 Cursor 开工。</li> <li><strong>人工 Review</strong>:这一步绝对不能省。我对 AI 的信任度还没到无脑 Merge 的地步,尤其是 E2E 测试覆盖率不够高的时候。我得盯着它生成的代码,脑子里过一遍逻辑。</li> <li><strong>收尾</strong>:测试,合并。</li> </ol> <p>这个流程看着挺顺,但有个大问题:<strong>我怎么把“复利”这一环加回来?</strong></p> <p>之前我看 Spec Driven Development(文档驱动开发),也就是 Speckit 或者 OpenSpec 那一套,试了一下就放弃了。感觉太重了,光是喂给 AI 的上下文就把窗口撑爆了,而且那种从头到尾写文档的感觉,很反直觉,完全没有了快速验证的快感。</p> <p>直到读到那篇<a href="https://mp.weixin.qq.com/s/CXx-0ar1EBf14vgQHHjU7A">《认知重建:Speckit 用了三个月,我放弃了》</a>,里面提到了<strong>复利工程</strong>的概念,简直说到我心坎里去了:</p> <pre><code>Plan ──────→ Work ──────→ Review ──────→ Compound 详细规划 执行工作 质量检查 知识沉淀 ↑ │ └───────────────────────────────────────┘ 知识复合:下次规划更精准 </code></pre> <p>这不就是我以前手工作坊时代的升级版吗?</p> <h4 id="3-在-cursor-中落地复利工程">3. 在 Cursor 中落地复利工程</h4> <p>理论有了,怎么在 Cursor 里实现?上个月 Cursor 2.4 更新了 SubAgents、Skills 和 Commands,我感觉机会来了。</p> <p>我先是扒了 <a href="https://github.com/affaan-m/everything-claude-code">everything-claude-code</a> 这个项目,想复刻一套工具链:</p> <ul> <li><strong>Subagent</strong>: planner, code-reviewer, doc-updater</li> <li><strong>Skill</strong>: security-review, tdd-workflow</li> <li><strong>Commands</strong>: learn(这个最关键,用于知识沉淀)</li> </ul> <p>理想很丰满,流程应该是 <code>planner -&gt; tdd-guide -&gt; code-reviewer -&gt; learn</code>。</p> <p>现实很骨感。Cursor 的 SubAgent 经常“自作主张”。你给它编排好了流程,它跑着跑着就切回自己的默认 Plan 模式,或者无视我的 SubAgent 逻辑。那一阵子搞得我很抓狂,感觉在跟一个不听话的实习生较劲。</p> <p>不过,<code>learn</code> 这个命令我是真留下来了。每次做完需求,把改动总结成知识点存下来,下次 AI 再写代码时,就能读到这些“家规”,确实有用。</p> <h4 id="4-最终的折衷方案">4. 最终的折衷方案</h4> <p>不死心的我又去研究了 <a href="https://github.com/obra/superpowers">superpowers</a> 和 <a href="https://github.com/sanyuan0704/code-review-expert">code-review-expert</a> 这两个项目,重新调整了策略。</p> <p>既然 Cursor 对 SubAgent 的调度有自己的想法,那我就顺着它,把流程简化,侧重于<strong>前置规划</strong>和<strong>后置学习</strong>。</p> <p>现在的工具组合变成了:</p> <ul> <li><strong>Skill</strong>: brainstorming, writing-plans, executing-plans, test-driven-development, subagent-driven-development, code-review-expert, using-superpowers</li> <li><strong>Commands</strong>: learn (雷打不动)</li> </ul> <p>流程图大概是这样:</p> <pre><code>brainstorming ──→ writing-plans ──→ executing-plans ──┬──→ test-driven-development └──→ code-review-expert ──→ learn </code></pre> <p>为了适配 Cursor,我把 Claude Code 定义的工具名都改了一遍,还在 Prompt 里强制要求它必须明确调用哪个 Skill。</p> <p>虽然现在 Cursor 还是偶尔会丢失上下文,或者写着写着断片了,需要我手动把断掉的节点接起来,但整体上,这套流程已经能复刻我以前的工作流了。</p> <p><strong>总结:</strong></p> <p>不管 AI 工具有多强,它终究是个工具。不要被工具牵着鼻子走,也不要为了用 AI 而用 AI。</p> <p>我觉得核心还是要把<strong>人</strong>的经验沉淀下来。以前我们沉淀在脑子里、Wiki 里,现在我们通过 <code>learn</code> 命令沉淀在本地文档里。</p> <p>AI 可以帮我们把 <code>Plan -&gt; Work</code> 这一段做得飞快,但 <code>Review -&gt; Compound</code> 这一段,目前还得靠我们自己把关。只有把这个闭环跑通了,才算真正把 AI 用成了自己的副驾驶,而不是请了个随时会跑路的外包。</p> 我的2025 https://zhu327.github.io/2026/01/04/%E6%88%91%E7%9A%842025/ Sun, 04 Jan 2026 10:00:00 +0800 https://zhu327.github.io/2026/01/04/%E6%88%91%E7%9A%842025/ <p>今天是2026年的第一个周日,窗外是珠海特有的温润海风。这一年,我从深圳到珠海,从一家迷茫的小公司到了老牌的金山办公。如果说2024年是本命年的阵痛,那么2025年对我来说,更像是在这激荡的技术浪潮中,努力寻找平衡与自洽的一年。</p> <p>依然按照惯例,絮絮叨叨地复盘下我的2025吧。</p> <h3 id="行云创新-及时止损的教训">行云创新:及时止损的教训</h3> <p>2025年的开头,我还在行云创新做云原生产品。当时做的是基于 K8s 的在线云 IDE,技术方案其实挺有意思:用 K8s 调度启动在线编辑器,用 code-server 和 selkies 暴露桌面 IDE,甚至还研究了 Docker Windows 容器。为了解决文件同步,我设计了 Sidecar 模式的文件服务,负责上传变更、初始化 git 目录这些活儿。</p> <p>技术上虽然有成就感,但职场环境却让我第一次深刻体会到“庙小妖风大”。</p> <p>先是内部莫名其妙的同事纠纷,随后是让人窒息的“服从性测试”。有一次我下班刚到家,就被领导一个电话叫回去值守,其实压根不是我的问题。最崩坏的是关于技术方向的讨论,领导要求我周末加班,把已经验证通过的方案改回到之前被废弃的死路上。我坚持了专业意见,并坦言周末搞不定,结果就是失去了所谓的“信任”。</p> <p></p> <p>在小公司,当领导不信任你,却又指望你干活时,事情会变得极其扭曲。他们开始通过能力欠缺的测试同学来传达意见。说实话,和理解能力、沟通能力都不在一个频次的人合作,真的非常痛苦。加上 2B 产品那种“为了投标乱堆定制功能”的短视,我意识到这里看不到未来。</p> <p><strong>教训很简单:不要对小公司抱有幻觉,更不要在让自己难受的环境里硬磨。</strong> 庆幸的是,年底和前同事聊天,发现我走后那里果然陷入了无意义的压榨。及时抽身,是我今年做的最正确的决定之一。</p> <h3 id="找工作-不匹配就是不匹配">找工作:不匹配就是不匹配</h3> <p>2月过完年,我重新回到了 Boss 直聘的战场。明显的感受是,互联网的盛况确实不再了,坑位少了很多。</p> <p>期间也面了一些不错的地方。比如一家做家具电商的新加坡公司,离家近,JD 也匹配。结果面试时考缓存一致性,我竟然脑子短路卡壳了,哈哈。还有影石360,面到了三轮,最后败在一道我没刷过的动态规划算法题上。</p> <p>其实面多了就想通了:<strong>面试不过,本质上就是不匹配。</strong> 不擅长手写算法是我一直以来的短板,而很多岗位需要的也不是我这种偏工程和架构的背景。最后,我投了广州的唯品会和珠海的金山办公。唯品会方向没对上,而金山办公这边,经过5轮面试定级P7,我重新回到了 SRE 的老本行。</p> <h3 id="金山办公-双城生活与新的大腿">金山办公:双城生活与新的大腿</h3> <p>6月入职金山,刚进去就遇到了一出“大厂政治剧”。我的直接领导是一位从腾讯过来的高P总监,本以为抱到了“鹅厂老乡”的大腿,结果他入职半个月、试用期快过的时候突然离职了。原因是外来的高P动了老员工的利益,被“兄弟文化”排挤。</p> <p>那一刻我真的很迷茫,简历这两年已经有点花了,要是这儿也待不住怎么办?好在新的 leader 就位后,我渐渐稳住了阵脚。我抛开了之前的屎山代码,开始在组内推广我推崇的“整洁架构”。</p> <p>现在的状态是,我手下带了2个人,还跨地域带了武汉的2个同事。我把更多精力放在了<strong>架构约束</strong>上:制定项目规范、拆解需求、帮同事解决技术卡点,努力不让项目走向不可控的混乱。</p> <p>最明显的变化是生活节奏。金山是真的不加班,早9晚6,非常养人。但我开启了“双城模式”:周一早起驱车去珠海,周五晚回深圳。对家人有愧疚,但下班后大把的独处时间,也让我看了不少以前没空看的电影,算是一种孤独的补偿吧。</p> <h3 id="vibe-coding-ai-时代的生存法则">Vibe Coding:AI 时代的生存法则</h3> <p>今年我最大的技术冲击来自于 AI。半年多前公司配了 Cursor,我正式开启了 <strong>Vibe Coding</strong> 模式。现在的流程是:我负责思考方案,写好文档,然后跟 AI 讨论可行性。</p> <p>我的工具链目前是这样的:</p> <ol> <li><strong>Google AI Studio (Gemini 3 Pro)</strong>:负责宏观架构,它上下文大,适合讨论长篇的实施计划。</li> <li><strong>Cursor (Claude Sonnet 4.5)</strong>:负责写代码,又快又好。</li> <li><strong>GPT-5.1 Codex High</strong>:负责 Review,它能抓到很多我忽略的边界细节。</li> </ol> <p>读代码也有了神器,<code>deepwiki.com</code> 理解大框架,<code>gitingest.com</code> 配合 Gemini 3 Pro 读仓库源码。在 Vibe Coding 时代,我发现<strong>程序员的“品味”变得空前重要</strong>。AI 会发散,如果没有严格的架构约束和清晰的规范,AI 产出的代码大概率也是一堆难以维护的屎山。</p> <h3 id="开源与折腾">开源与折腾</h3> <p>有了闲暇时间,开源项目也搞得风生水起。</p> <ul> <li><strong>Pingsix</strong>:写了20多个插件,被 GPT-5.1 捉了十几个虫,解锁了Ingress Controller。</li> <li><strong>Bookmark</strong>:趁着 Pocket 关闭,我把老书签整理了,顺便用 AI 做了自动化总结。</li> <li><strong>NAS</strong>:家里的 RK3399 还在服役,换了 SSD,折腾了联通 IPv6。现在结合夸克网盘和 TG 机器人,追剧已经成了全自动的享受。</li> </ul> <p>甚至我还精简了银行卡,把一堆乱七八糟的卡消了。现在主刷农行精粹白和工行香格里拉白。理财归微众,工资归招行,生活变得简单了不少。</p> <h3 id="结尾">结尾</h3> <p>2025年,我在珠海的夕阳里思考过职业危机,也在双城的高速上想念过家人。虽然简历看上去不再那么“大厂精英”,但我对编程的热爱反而因为 AI 的介入变得更纯粹了——我不再纠结于繁琐的语法,而是更专注于<strong>解决问题</strong>本身。</p> <p>未来依然不确定,但就像我折腾 OpenWrt 拨号一样,掉线了就重新拨号,断路了就换个方案。只要逻辑还在,系统总能 Run 起来。</p> <p>再见,2025。你好,2026。</p> AI Agent 工程化实践:从 Prompt 到 Context 的思维转变 https://zhu327.github.io/2025/11/30/ai-agent-%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%E4%BB%8E-prompt-%E5%88%B0-context-%E7%9A%84%E6%80%9D%E7%BB%B4%E8%BD%AC%E5%8F%98/ Sun, 30 Nov 2025 10:00:00 +0800 https://zhu327.github.io/2025/11/30/ai-agent-%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%E4%BB%8E-prompt-%E5%88%B0-context-%E7%9A%84%E6%80%9D%E7%BB%B4%E8%BD%AC%E5%8F%98/ <p>最近在新公司这边,大部分精力都耗在了 AI Agent 的落地应用上。大家都在谈 Agent,网上的 Demo 也是满天飞,但当你真正把这玩意儿往生产环境搬的时候,会发现完全是两码事。</p> <p>简单的说,AI Agent 的核心逻辑其实并不复杂,本质上就是一个基于 ReAct(Reasoning and Acting)范式的工程结构,程序在一个死循环里不断地调用 LLM 的 API,让它根据当前的观测去决策下一步是思考还是行动。</p> <p>起初我也觉得这就跟写个脚本差不多,但在实际调试过程中,我发现让 Agent “跑通”不难,但要让它“跑好”、“省钱”且“不犯蠢”,这里面全是工程细节。我也复盘了一下这段时间的踩坑经历,总结了一些在工程实践中的“巧思”。</p> <p></p> <h3 id="遇到的瓶颈">遇到的瓶颈</h3> <p>在项目初期,我碰到的最大痛点就是 <strong>Token 的消耗</strong> 和 <strong>上下文窗口(Context Window)的限制</strong>。</p> <p>Agent 运行起来后,为了保证它能记住之前的操作,我们往往会把历史对话一股脑地塞进 Prompt 里。但随着交互轮数的增加,上下文会迅速膨胀。这不仅烧钱(Token 费很贵),更要命的是,当无关信息太多时,LLM 的注意力会被稀释,导致它开始“幻觉”或者逻辑混乱,也就是我们常说的“变笨了”。</p> <p>为了解决这个问题,我开始从工程角度去干预 Agent 的记忆机制,我发现单纯的 Prompt Engineering 已经不够用了,我们需要进阶到 <strong>Context Engineering(上下文工程)</strong>。</p> <h3 id="几个工程化的巧思">几个工程化的巧思</h3> <p>针对上下文过载和执行效率低的问题,我在工程实践中尝试了以下几个优化策略,效果还不错:</p> <h4 id="1-动态整理与压缩上下文">1. 动态整理与压缩上下文</h4> <p>这招主要是应对“记不住”的问题。当对话轮数逼近模型的 Context 上限时,如果直接截断最早的记录,Agent 可能会丢失关键的任务背景。</p> <p>我的做法是,在检测到 Token 数快到阈值时,触发一个后台任务,让 LLM 自己对之前的多轮对话做一个动态整理(Summary)。</p> <ul> <li><strong>输入</strong>:过去 N 轮的详细对话。</li> <li><strong>输出</strong>:提取出“用户的核心诉求”、“已完成的关键步骤”和“当前获得的中间结果”。</li> </ul> <p>用这段高密度的摘要替换掉那 N 轮冗长的对话。这样既大幅减少了 Token 量,又确保了历史关键信息得以保存,让 Agent 始终记得“我是谁,我在哪,我要干什么”。</p> <h4 id="2-checkpoint-回溯与有效信息保留">2. Checkpoint 回溯与有效信息保留</h4> <p>Agent 在尝试调用工具时,并不总是成功的。有时候它参数传错了,有时候 API 报错了。在标准的 ReAct 循环里,这些报错信息(Error Logs)会被完整地记录在上下文里。</p> <p>这就导致一个问题:如果 Agent 试错了好几次才成功,那上下文里会充斥着大量的垃圾报错信息。这些信息对于后续的决策不仅无用,反而可能误导模型。</p> <p>这里我引入了一个类似游戏存档的 <strong>Checkpoint(检查点)</strong> 机制。</p> <ul> <li>在执行关键动作前,系统会自动保存当前的上下文快照。</li> <li>当检测到 Agent 陷入死循环或产生大量无效交互时,系统会强制回滚到上一个 Checkpoint。</li> <li><strong>关键点</strong>:回溯不是简单的“读档重来”,我们会通过规则提取出刚才试错过程中产生的“有效信息”(比如“某个参数验证失败”的结论),将其注入到回溯后的上下文中。</li> </ul> <p>这样既清洗了上下文中的噪音,又保留了试错的价值,避免 Agent 在同一个坑里跌倒两次。</p> <h4 id="3-sub-agent-解决上下文隔离">3. Sub Agent 解决上下文隔离</h4> <p>在某些场景下,Agent 需要执行非常复杂的子任务,比如写一段 Python 代码并执行,或者进行复杂的数据清洗。</p> <p>如果把这些子任务的所有中间步骤都塞进主线程的上下文(Main Context),瞬间就会把 Token 撑爆。我的解决思路是引入 <strong>Sub Agent(子智能体)</strong>:</p> <ul> <li>把复杂任务分发给一个独立的 Sub Agent。</li> <li>Sub Agent 拥有独立的上下文空间,它可以在里面进行多轮的思考、调试、修正。</li> <li>当 Sub Agent 任务完成后,只提取“最终结果”返回给主 Agent。</li> </ul> <p>通过这种方式,我们实现了上下文的物理隔离,主 Agent 的视野始终保持清爽,只关注宏观流程,而细节则由 Sub Agent 屏蔽。</p> <h4 id="4-面向-人-设计工具-而非面向-api">4. 面向“人”设计工具,而非面向 API</h4> <p>这一点是我觉得思维层次提升最大的地方。</p> <p>最初给 Agent 设计工具时,我习惯直接把后端的原子 API 扔给它,比如 <code>login()</code>, <code>get_token()</code>, <code>get_user_info()</code>。结果就是 Agent 完成一个简单的查用户信息,需要来回跑三趟。</p> <p>后来我意识到,Agent 使用工具的逻辑应该更像“人”使用 app。工具的设计粒度应该更粗。我们将多个原子 API 组合封装成一个面向业务场景的高阶工具。这样一次调用就能搞定,减少了 Agent 的交互流程,容错率也大大提升。</p> <h3 id="总结-从-prompt-到-context">总结:从 Prompt 到 Context</h3> <p>回顾这些优化点,我们可以发现,提升 Agent 能力的关键,不仅仅在于你 Prompt 写得有多花哨,更在于你如何管理它所能看到的“世界”。</p> <p>让我们来总结下这里的思维层次:</p> <ul> <li><strong>第一层(Prompt Engineering)</strong>:专注于怎么把 Prompt 词写好,试图用一段话把所有要求都告诉 AI。</li> <li><strong>第二层(RAG / Memory)</strong>:开始给 AI 外挂数据库,解决知识库的问题。</li> <li><strong>第三层(Context Engineering)</strong>:把上下文看作一种稀缺的计算资源,通过动态压缩、Checkpoint 回溯、Sub Agent 隔离等工程手段,动态地管理 AI 的注意力。</li> <li><strong>第四层(Architecture Design)</strong>:从单点优化走向系统设计,构建一套具备自我纠错、状态管理、分层协作的智能体运行时环境。</li> </ul> <p>以前做传统开发,我们优化的是 CPU 和内存;现在做 AI 开发,我们优化的其实是 Context Window 和 Token 效率。</p> <p>在这个技术日新月异的时代,作为程序员,我们不仅要会写代码,更要学会如何设计系统来弥补模型的短板。这些关于 Context 的巧思,其实就是把复杂的非结构化问题,通过工程手段变得有序化的过程。</p> <p>希望这些瞎絮叨的经验能对正在折腾 Agent 的你有那么一点点启发。2025 年了,保持学习,保持思考,依然是应对变化的唯一解法。</p> 系统性思维的陷阱:从“完美方案”到“有效落地” https://zhu327.github.io/2025/11/21/%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%80%9D%E7%BB%B4%E7%9A%84%E9%99%B7%E9%98%B1%E4%BB%8E%E5%AE%8C%E7%BE%8E%E6%96%B9%E6%A1%88%E5%88%B0%E6%9C%89%E6%95%88%E8%90%BD%E5%9C%B0/ Fri, 21 Nov 2025 10:00:52 +0800 https://zhu327.github.io/2025/11/21/%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%80%9D%E7%BB%B4%E7%9A%84%E9%99%B7%E9%98%B1%E4%BB%8E%E5%AE%8C%E7%BE%8E%E6%96%B9%E6%A1%88%E5%88%B0%E6%9C%89%E6%95%88%E8%90%BD%E5%9C%B0/ <p>我做程序员已经 10 多年了。</p> <p>回想刚入行那会儿,我的思维模式很简单:产品经理提什么,我就做什么。那时候追求的是“短平快”,像个执行指令的机器,代码写得快就是牛。</p> <p>后来加入了腾讯,随着职级的提升,负责的系统越来越复杂,我开始意识到:写代码只是冰山一角,<strong>做方案才是真正的考验。</strong></p> <p>所谓的“系统性思维”,一度让我受益匪浅,但最近我发现,如果一旦陷入对“完美”的执念,系统性思维反而会成为阻碍项目落地的最大陷阱。今天想和大家聊聊,如何在架构设计、风险控制和实际落地之间找到那个微妙的平衡点。</p> <p></p> <h3 id="方案的本质-平衡客观条件的艺术">方案的本质:平衡客观条件的艺术</h3> <p>什么是做方案?以前我觉得是画一张最漂亮的架构图,用最牛的组件。现在我理解,<strong>做方案其实是把各种“客观条件”作为输入,寻找最优解的过程。</strong></p> <p>这些客观条件包括:</p> <ul> <li><strong>硬件资源</strong>:手头有多少服务器?</li> <li><strong>依赖组件</strong>:公司基建支持什么?(比如你想用 ClickHouse,但公司运维没经验,出了问题没人兜底,那这就不是一个好选项,不如看看现有组件能不能替代。)</li> <li><strong>人力成本</strong>:团队几个人?水平如何?</li> <li><strong>时间窗口</strong>:业务等得起多久?</li> </ul> <p>我见过很多“完美”的方案,逻辑自洽,架构优雅,但落地需要半年。而另一个方案虽然略显粗糙,但只需要 1 个月,且预留了后续演进的空间。在商业世界里,后者往往才是胜者。</p> <p><strong>架构不是设计出来的,是演进出来的。</strong> 我们需要做的是调整各种客观条件的优先级,在风险可控的前提下,通过多方评审(集思广益,消除盲区),找到那个平衡点。</p> <h3 id="实战案例-只解决-80-的问题">实战案例:只解决 80% 的问题</h3> <p>分享一个我在腾讯做权限中心时的真实案例。</p> <p>当时我们面临一个痛点:有一个中心化的管理员,每天大量时间都在处理审批单,他一直在吐槽,希望把这些审批自动路由到对应的业务负责人,别全堆在他头上。</p> <p>我第一反应是:<strong>做不到。</strong></p> <p>从技术角度看,审批范围是一个复杂的表达式。要根据用户申请的数据去匹配这个表达式,没法直接做数据库索引。如果要实现,就得全表扫描进行遍历匹配,性能扛不住。而且,很多申请范围很模糊,根本匹配不到具体负责人,最后还是得兜底回落到管理员那里。</p> <p>于是这个需求被我挡回去了。后来问题太严重,捅到了上面。在复盘会上,我的 Leader 问了一句话,让我瞬间顿悟:</p> <p><strong>“能不能在只解决 80% 问题的前提下,来做这个方案?”</strong></p> <p>我突然意识到,我陷入了“完美主义”的陷阱。我一直在纠结那 20% 匹配不到的情况,却忽略了如果能自动化处理掉 80% 的单子,管理员的工作量就能大大减轻。</p> <p>于是我重新设计了方案:</p> <ol> <li><strong>引入前置索引</strong>:提取表达式中的关键特征做索引。</li> <li><strong>允许降级</strong>:能走索引匹配的先走索引,匹配不到的(那复杂的 20%),直接回退给中心管理员。</li> </ol> <p>事实证明,方案落地后,管理员的压力骤减。这就告诉我:<strong>有时候真的没有完美的方案,只有平衡现状的方案。</strong></p> <h3 id="开源代码的-神话-与重构">开源代码的“神话”与重构</h3> <p>在阅读优秀开源项目代码时,我常有这种时刻:“哇塞,这代码写得太神了,连这种极端场景都想到了,作者是开了天眼吗?”</p> <p>随着我自己经验的增长,我发现所谓的“神人”并没有那么玄乎。</p> <p>他能考虑到那个场景,很可能只是因为他<strong>踩过那个坑</strong>。</p> <ul> <li>版本 1.0 可能是个草台班子。</li> <li>版本 2.0 修复了几个致命 Bug(踩坑)。</li> <li>版本 3.0 因为补丁打多了,代码太烂,被迫重构,把之前的坑变成了新的设计约束。</li> </ul> <p>我在腾讯的 6 年里,权限中心整体重构过 3 次,API 网关也重构过 3 次。每一次重构,都是因为旧的方案无法适应新的客观条件。</p> <p><strong>踩坑并不是坏事,失败也是财富。</strong> 那些让你绞尽脑汁都想不到的 Edge Case,往往是别人血淋淋的经验积累。我们在做方案时,不要怕现在的方案未来会被推翻,<strong>只要预留了演进空间,现在的“不完美”就是未来能力的基石。</strong></p> <h3 id="警惕-过度系统化-的僵局">警惕“过度系统化”的僵局</h3> <p>我现在所在的公司,领导层是运维出身。运维思维和研发思维有一个本质的区别:<strong>运维天然厌恶风险(Risk Averse),而研发需要管理风险。</strong></p> <p>这就导致了一个尴尬的局面: 我提出了方案 A 来解决问题 A,但在评审会上,领导会挑战:“你这个方案没有解决问题 B 怎么办?有没有风险?”</p> <p>于是,技术评审会变成了“挑刺大会”。</p> <ul> <li>为了应对领导的挑刺,我们被迫把方案搞得越来越复杂,试图覆盖所有角落。</li> <li>方案一复杂,落地难度就指数级上升。</li> <li>最终结果是:动作变形,无法落地,不了了之。</li> </ul> <p>组内甚至有同事因为方案迟迟推不动,最后无奈活水走人。</p> <p>这种<strong>过度系统化</strong>的思考方式,实际上是一种<strong>停滞</strong>。领导当然需要系统性思考,但如果一直纠结于“一步到位”的完美方案,就会导致方案根本不具备可行性。</p> <h3 id="我的应对策略-最小化闭环">我的应对策略:最小化闭环</h3> <p>面对这种环境,如果硬刚,很容易陷入内耗。我的应对方法是:<strong>MVP(Minimum Viable Product)策略 + 工具提效。</strong></p> <ol> <li><strong>把方案切得足够小</strong>:不要试图画一张大饼,而是聚焦当前最核心的痛点。</li> <li><strong>快速跑通 PoC(概念验证)</strong>:与其在会议室争论风险,不如直接把代码跑起来,让领导看到可视化的成果。</li> <li><strong>利用 AI 提效</strong>:为了不让自己太累,我现在大量使用 AI 编程工具。它可以帮我快速生成原型代码,大大缩短从“想法”到“演示”的时间。</li> </ol> <p>虽然这样做确实会累一点(因为要先斩后奏,自己承担一部分验证成本),但在“不做不错”和“艰难推进”之间,我选择后者。</p> <h3 id="写在最后">写在最后</h3> <p>系统性思维是好东西,它让我们思考得更全面。但我们不能让系统性思维成为<strong>阻碍系统发展</strong>的借口。</p> <p>做方案,本质上是在<strong>不确定性中寻找确定性</strong>。</p> <p>既要有思维高度,也要有“先解决当下问题”的务实态度。不要害怕方案不完美,<strong>只要控制好风险,让系统转起来,剩下的,交给时间去演进。</strong></p> pingsix-ingress-controller启动 https://zhu327.github.io/2025/10/20/pingsix-ingress-controller%E5%90%AF%E5%8A%A8/ Mon, 20 Oct 2025 10:00:52 +0800 https://zhu327.github.io/2025/10/20/pingsix-ingress-controller%E5%90%AF%E5%8A%A8/ <blockquote> <p><a href="https://github.com/zhu327/pingsix">https://github.com/zhu327/pingsix</a></p> </blockquote> <p>PingSIX 自从上次重构之后, 我一直在考虑如何扩展这个项目的功能与使用场景, 有2个方向可以考虑:</p> <ol> <li>支持proxy-wasm插件</li> <li>实现pingsix-ingress-controller</li> </ol> <p>在调研了proxy-wasm的相关功能后, 结论是由于pingora面向的场景是CDN的反向代理, 所以没有考虑过方便的修改请求体与响应体, 这就造成很难基于pingora来实现proxy-wasm的ABI, 如果我要自己定义一个wasm的接口协议, 没法复用社区现有的proxy-wasm插件, 那就没必要了, 不如直接写Rust的插件. 相关参考内容:</p> <ul> <li><a href="https://github.com/cloudflare/pingora/issues/17">proxy-wasm support</a></li> <li><a href="https://segmentfault.com/a/1190000045232953">pingora 能做什么和不能做什么</a></li> </ul> <p>在放弃了proxy-wasm的支持后, 我开始调研如何实现pingsix-ingress-controller, 由于pingsix的的资源定义是参考apisix来实现的, 所以就直接参考<a href="https://github.com/apache/apisix-ingress-controller">apisix-ingress-controller</a>来实现我们自己的<a href="https://github.com/zhu327/pingsix-ingress-controller">pingsix-ingress-controller</a>.</p> <p></p> <h3 id="1-apisix-ingress-controller架构">1. apisix-ingress-controller架构</h3> <p><img src="https://github.com/user-attachments/assets/1ecf116a-378f-4357-a597-5bafb56991fd" alt="Image1" width="700px" /></p> <h4 id="1-k8s-resources-watch-layer-资源监听层">1. K8s Resources Watch Layer (资源监听层)</h4> <ul> <li>各种Controller通过Kubernetes的Watch机制监听对应的资源变化</li> <li>支持的资源类型包括: <ul> <li>Gateway API: HTTPRoute, Gateway, GRPCRoute, TCPRoute, UDPRoute, TLSRoute</li> <li>Kubernetes原生: Ingress, IngressClass</li> <li>APISIX CRD: ApisixRoute, ApisixGlobalRule, ApisixTls, ApisixConsumer, ApisixUpstream</li> <li>自定义: Consumer, GatewayProxy</li> </ul></li> </ul> <h4 id="2-provider-layer-提供者层">2. Provider Layer (提供者层)</h4> <ul> <li>Provider接收Controller的Update/Delete请求</li> <li>TranslateContext收集所有依赖资源(Services, Secrets, EndpointSlices等)</li> <li>为Translator提供完整的上下文信息</li> </ul> <h4 id="3-translator-layer-翻译层">3. Translator Layer (翻译层)</h4> <ul> <li>将K8s资源翻译成ADC资源描述</li> <li>每种资源类型都有对应的Translator方法</li> <li>输出TranslateResult,包含: <ul> <li>Services (路由规则)</li> <li>SSL/TLS (证书)</li> <li>Consumers (消费者)</li> <li>GlobalRules (全局规则)</li> <li>PluginMetadata (插件元数据)</li> </ul></li> </ul> <h4 id="4-adc-client-layer-adc客户端层">4. ADC Client Layer (ADC客户端层)</h4> <ul> <li><strong>ConfigManager</strong>: 管理多个GatewayProxy的配置</li> <li><strong>Store/MemDB</strong>: 内存数据库,存储ADC资源状态</li> <li><strong>StoreDelta</strong>: 对比新旧配置,计算差异</li> <li>将变更任务传递给Executor执行</li> </ul> <h4 id="5-adc-executor-interface-执行器接口">5. ADC Executor Interface (执行器接口)</h4> <p>ADC Executor提供统一的Execute接口,支持三种实现:</p> <ul> <li><strong>HTTPADCExecutor</strong>: 通过HTTP调用ADC Server(推荐方式)</li> <li><strong>DefaultADCExecutor</strong>: 通过命令行调用adc命令</li> </ul> <h4 id="6-adc-http-server-adc-http服务器">6. ADC HTTP Server (ADC HTTP服务器)</h4> <p>ADC HTTP Server的核心流程:</p> <ol> <li>接收<code>/sync</code>端点的PUT请求</li> <li>解析ADCServerRequest(包含opts和config)</li> <li>根据label-selector从APISIX拉取现有资源</li> <li>将ADC资源描述转换为APISIX资源格式</li> <li>对比已拉取的资源,计算差异(Diff)</li> <li>调用APISIX Admin API执行创建/更新/删除操作</li> <li>返回SyncResult(包含成功/失败状态)</li> </ol> <h4 id="7-apisix-data-plane-apisix数据平面">7. APISIX Data Plane (APISIX数据平面)</h4> <p>最终在APISIX中创建/更新/删除的资源:</p> <ul> <li>Routes (路由)</li> <li>Services (服务)</li> <li>Upstreams (上游)</li> <li>SSL/TLS (证书)</li> <li>Consumers (消费者)</li> <li>Global Rules (全局规则)</li> <li>Plugin Metadata (插件元数据)</li> </ul> <h4 id="8-核心特性">8. 核心特性</h4> <ol> <li><strong>多GatewayProxy支持</strong>: 通过ConfigManager管理多个APISIX实例的配置</li> <li><strong>Label Selector</strong>: 支持通过标签选择器过滤资源</li> <li><strong>增量同步</strong>: 通过MemDB对比差异,只同步变更的资源</li> <li><strong>错误处理</strong>: 完善的错误收集和状态更新机制</li> <li><strong>灵活的执行方式</strong>: 支持HTTP、命令行多种执行模式</li> <li><strong>资源隔离</strong>: 通过label实现不同资源的隔离和管理</li> </ol> <h3 id="2-apisix-ingress-controller的经验">2. apisix-ingress-controller的经验</h3> <p>虽然以前也写过一些operator的代码, 但是在学习apisix-ingress-controller代码的过程中, 我还是学到了一些新的东西, 在controller watch一类资源的时候, 我们可以watch所有关联的资源类型, 一旦这些关联的资源类型有变更, 就可以进入统一的变更流程, 这样就减少了我们写controller的逻辑复杂度, 所有的关联资源的变更都会触发主资源的更新.</p> <div class="highlight"><pre class="chroma"><code class="language-golang" data-lang="golang"><span class="c1">// SetupWithManager sets up the controller with the Manager. </span><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">IngressReconciler</span><span class="p">)</span> <span class="nx">SetupWithManager</span><span class="p">(</span><span class="nx">mgr</span> <span class="nx">ctrl</span><span class="p">.</span><span class="nx">Manager</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> <span class="nx">r</span><span class="p">.</span><span class="nx">genericEvent</span> <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">event</span><span class="p">.</span><span class="nx">GenericEvent</span><span class="p">,</span> <span class="mi">100</span><span class="p">)</span> <span class="k">return</span> <span class="nx">ctrl</span><span class="p">.</span><span class="nx">NewControllerManagedBy</span><span class="p">(</span><span class="nx">mgr</span><span class="p">).</span> <span class="nx">For</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">Ingress</span><span class="p">{},</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span> <span class="nx">MatchesIngressClassPredicate</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">Client</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Log</span><span class="p">),</span> <span class="p">),</span> <span class="p">).</span> <span class="nx">WithEventFilter</span><span class="p">(</span> <span class="nx">predicate</span><span class="p">.</span><span class="nx">Or</span><span class="p">(</span> <span class="nx">predicate</span><span class="p">.</span><span class="nx">GenerationChangedPredicate</span><span class="p">{},</span> <span class="nx">predicate</span><span class="p">.</span><span class="nx">AnnotationChangedPredicate</span><span class="p">{},</span> <span class="nx">predicate</span><span class="p">.</span><span class="nx">NewPredicateFuncs</span><span class="p">(</span><span class="nx">TypePredicate</span><span class="p">[</span><span class="o">*</span><span class="nx">corev1</span><span class="p">.</span><span class="nx">Secret</span><span class="p">]()),</span> <span class="p">),</span> <span class="p">).</span> <span class="nx">Watches</span><span class="p">(</span> <span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">IngressClass</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressForIngressClass</span><span class="p">),</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span> <span class="nx">predicate</span><span class="p">.</span><span class="nx">NewPredicateFuncs</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">matchesIngressController</span><span class="p">),</span> <span class="p">),</span> <span class="p">).</span> <span class="nx">Watches</span><span class="p">(</span> <span class="o">&amp;</span><span class="nx">discoveryv1</span><span class="p">.</span><span class="nx">EndpointSlice</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesByService</span><span class="p">),</span> <span class="p">).</span> <span class="nx">Watches</span><span class="p">(</span> <span class="o">&amp;</span><span class="nx">corev1</span><span class="p">.</span><span class="nx">Secret</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesBySecret</span><span class="p">),</span> <span class="p">).</span> <span class="nx">Watches</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">v1alpha1</span><span class="p">.</span><span class="nx">BackendTrafficPolicy</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressForBackendTrafficPolicy</span><span class="p">),</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span> <span class="nx">BackendTrafficPolicyPredicateFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">genericEvent</span><span class="p">),</span> <span class="p">),</span> <span class="p">).</span> <span class="nx">Watches</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">v1alpha1</span><span class="p">.</span><span class="nx">HTTPRoutePolicy</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesByHTTPRoutePolicy</span><span class="p">),</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span><span class="nx">httpRoutePolicyPredicateFuncs</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">genericEvent</span><span class="p">)),</span> <span class="p">).</span> <span class="nx">Watches</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">v1alpha1</span><span class="p">.</span><span class="nx">GatewayProxy</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesForGatewayProxy</span><span class="p">),</span> <span class="p">).</span> <span class="nx">WatchesRawSource</span><span class="p">(</span> <span class="nx">source</span><span class="p">.</span><span class="nx">Channel</span><span class="p">(</span> <span class="nx">r</span><span class="p">.</span><span class="nx">genericEvent</span><span class="p">,</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressForGenericEvent</span><span class="p">),</span> <span class="p">),</span> <span class="p">).</span> <span class="nx">Complete</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span> <span class="p">}</span></code></pre></div> <p>IngressReconciler Watch 事件详解</p> <p>根据代码分析,<code>IngressReconciler</code> 在 <code>SetupWithManager</code> 方法中配置了多个 watch 事件。让我为您详细解释每个事件的作用和逻辑:</p> <h4 id="1-主资源-watch-ingress-第70-81行">1. <strong>主资源 Watch - Ingress</strong> (第70-81行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">For</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">Ingress</span><span class="p">{},</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span> <span class="nx">MatchesIngressClassPredicate</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">Client</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Log</span><span class="p">),</span> <span class="p">))</span></code></pre></div> <p><strong>作用</strong>:监听 Ingress 资源本身的变化</p> <p><strong>Predicates(过滤条件)</strong>:</p> <ul> <li><code>MatchesIngressClassPredicate</code>: 只处理由当前控制器管理的 IngressClass 的 Ingress</li> <li><code>GenerationChangedPredicate</code>: 资源的 Generation 发生变化(spec 修改)</li> <li><code>AnnotationChangedPredicate</code>: 注解发生变化</li> <li><code>TypePredicate[*corev1.Secret]()</code>: 用于 Secret 类型判断</li> </ul> <p><strong>逻辑</strong>:这是主要的监听对象,当 Ingress 的规格或注解变化时触发 Reconcile</p> <h4 id="2-ingressclass-watch-第82-88行">2. <strong>IngressClass Watch</strong> (第82-88行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">Watches</span><span class="p">(</span> <span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">IngressClass</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressForIngressClass</span><span class="p">),</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span> <span class="nx">predicate</span><span class="p">.</span><span class="nx">NewPredicateFuncs</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">matchesIngressController</span><span class="p">),</span> <span class="p">),</span> <span class="p">)</span></code></pre></div> <p><strong>作用</strong>:监听 IngressClass 资源的变化</p> <p><strong>触发条件</strong>:</p> <ul> <li><code>matchesIngressController</code>: 只监听由当前控制器管理的 IngressClass(通过 <code>spec.controller</code> 字段匹配)</li> </ul> <p><strong>逻辑</strong> (<code>listIngressForIngressClass</code>):</p> <ol> <li>检查 IngressClass 是否是默认类(通过注解 <code>ingressclass.kubernetes.io/is-default-class</code>)</li> <li>如果是默认类:列出所有未指定 IngressClassName 或指定为该类的 Ingress</li> <li>如果不是默认类:通过索引查找使用该 IngressClass 的所有 Ingress</li> <li>返回需要 reconcile 的 Ingress 列表</li> </ol> <p><strong>使用场景</strong>:当 IngressClass 的配置变化时,需要重新处理所有使用该类的 Ingress</p> <h4 id="3-endpointslice-watch-第89-92行">3. <strong>EndpointSlice Watch</strong> (第89-92行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">Watches</span><span class="p">(</span> <span class="o">&amp;</span><span class="nx">discoveryv1</span><span class="p">.</span><span class="nx">EndpointSlice</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesByService</span><span class="p">),</span> <span class="p">)</span></code></pre></div> <p><strong>作用</strong>:监听后端服务的 Endpoint 变化</p> <p><strong>逻辑</strong> (<code>listIngressesByService</code>):</p> <ol> <li>从 EndpointSlice 的 label 中提取 Service 名称(<code>discovery.k8s.io/service-name</code>)</li> <li>通过索引 <code>ServiceIndexRef</code> 查找引用该 Service 的所有 Ingress</li> <li>过滤出由当前控制器管理的 Ingress</li> <li>返回需要 reconcile 的 Ingress 列表</li> </ol> <p><strong>使用场景</strong>:当后端 Pod 的 IP 地址变化(扩缩容、重启等)时,需要更新 APISIX 的 upstream 配置</p> <h4 id="4-secret-watch-第93-96行">4. <strong>Secret Watch</strong> (第93-96行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">Watches</span><span class="p">(</span> <span class="o">&amp;</span><span class="nx">corev1</span><span class="p">.</span><span class="nx">Secret</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesBySecret</span><span class="p">),</span> <span class="p">)</span></code></pre></div> <p><strong>作用</strong>:监听 TLS 证书 Secret 的变化</p> <p><strong>逻辑</strong> (<code>listIngressesBySecret</code>):</p> <ol> <li>通过索引 <code>SecretIndexRef</code> 查找直接引用该 Secret 的 Ingress(TLS 配置)</li> <li>查找引用该 Secret 的 GatewayProxy(用于 provider 认证)</li> <li>如果 GatewayProxy 引用了该 Secret,找到使用该 GatewayProxy 的 IngressClass</li> <li>再找到使用这些 IngressClass 的所有 Ingress</li> <li>去重后返回所有需要 reconcile 的 Ingress</li> </ol> <p><strong>使用场景</strong>:</p> <ul> <li>TLS 证书更新或轮换</li> <li>GatewayProxy 的 AdminKey Secret 变化</li> </ul> <h4 id="5-backendtrafficpolicy-watch-第97-102行">5. <strong>BackendTrafficPolicy Watch</strong> (第97-102行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">Watches</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">v1alpha1</span><span class="p">.</span><span class="nx">BackendTrafficPolicy</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressForBackendTrafficPolicy</span><span class="p">),</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span> <span class="nx">BackendTrafficPolicyPredicateFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">genericEvent</span><span class="p">),</span> <span class="p">),</span> <span class="p">)</span></code></pre></div> <p><strong>作用</strong>:监听后端流量策略的变化</p> <p><strong>Predicates 逻辑</strong>:</p> <ul> <li><strong>Create</strong>: 返回 true,新建时触发</li> <li><strong>Delete</strong>: 返回 true,删除时触发</li> <li><strong>Update</strong>: 检测 <code>targetRefs</code> 的变化 <ul> <li>找出被移除的 targetRefs</li> <li>将包含被移除 targetRefs 的旧对象发送到 genericEvent channel</li> <li>这样可以清理不再被引用的资源</li> </ul></li> </ul> <p><strong>逻辑</strong> (<code>listIngressForBackendTrafficPolicy</code>):</p> <ol> <li>遍历 Policy 的所有 <code>targetRefs</code>(引用的 Service)</li> <li>通过索引查找使用这些 Service 的 Ingress</li> <li>去重后返回需要 reconcile 的 Ingress 列表</li> </ol> <p><strong>使用场景</strong>:配置后端流量策略(如负载均衡算法、健康检查等)</p> <h4 id="6-httproutepolicy-watch-第103-106行">6. <strong>HTTPRoutePolicy Watch</strong> (第103-106行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">Watches</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">v1alpha1</span><span class="p">.</span><span class="nx">HTTPRoutePolicy</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesByHTTPRoutePolicy</span><span class="p">),</span> <span class="nx">builder</span><span class="p">.</span><span class="nx">WithPredicates</span><span class="p">(</span><span class="nx">httpRoutePolicyPredicateFuncs</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">genericEvent</span><span class="p">)),</span> <span class="p">)</span></code></pre></div> <p><strong>作用</strong>:监听 HTTP 路由策略的变化</p> <p><strong>Predicates 逻辑</strong>:</p> <ul> <li><strong>Create/Delete</strong>: 返回 true</li> <li><strong>Update</strong>: 检测 <code>targetRefs</code> 的变化 <ul> <li>找出被移除的 targetRefs</li> <li>将包含被移除 targetRefs 的旧对象发送到 genericEvent channel</li> </ul></li> </ul> <p><strong>逻辑</strong> (<code>listIngressesByHTTPRoutePolicy</code>):</p> <ol> <li>遍历 Policy 的所有 <code>targetRefs</code></li> <li>过滤出 Kind 为 <code>Ingress</code> 的引用</li> <li>获取这些 Ingress 对象</li> <li>返回需要 reconcile 的 Ingress 列表</li> </ol> <p><strong>使用场景</strong>:配置 HTTP 路由级别的策略(如重写、重定向、超时等)</p> <h4 id="7-gatewayproxy-watch-第107-109行">7. <strong>GatewayProxy Watch</strong> (第107-109行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">Watches</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">v1alpha1</span><span class="p">.</span><span class="nx">GatewayProxy</span><span class="p">{},</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressesForGatewayProxy</span><span class="p">),</span> <span class="p">)</span></code></pre></div> <p><strong>作用</strong>:监听 GatewayProxy 配置的变化</p> <p><strong>逻辑</strong> (<code>listIngressesForGatewayProxy</code> -&gt; <code>listIngressClassRequestsForGatewayProxy</code>):</p> <ol> <li>通过索引 <code>IngressClassParametersRef</code> 查找引用该 GatewayProxy 的 IngressClass</li> <li>对每个 IngressClass,调用 <code>listIngressForIngressClass</code> 获取相关 Ingress</li> <li>去重后返回所有需要 reconcile 的 Ingress</li> </ol> <p><strong>使用场景</strong>:</p> <ul> <li>GatewayProxy 的 APISIX 地址变化</li> <li>发布服务配置变化</li> <li>Provider 配置变化</li> </ul> <h4 id="8-generic-event-channel-第110-116行">8. <strong>Generic Event Channel</strong> (第110-116行)</h4> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="nx">WatchesRawSource</span><span class="p">(</span> <span class="nx">source</span><span class="p">.</span><span class="nx">Channel</span><span class="p">(</span> <span class="nx">r</span><span class="p">.</span><span class="nx">genericEvent</span><span class="p">,</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">EnqueueRequestsFromMapFunc</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">listIngressForGenericEvent</span><span class="p">),</span> <span class="p">),</span> <span class="p">)</span></code></pre></div> <p><strong>作用</strong>:处理通过 channel 发送的自定义事件</p> <p><strong>逻辑</strong> (<code>listIngressForGenericEvent</code>):</p> <ul> <li>根据对象类型路由到相应的处理函数: <ul> <li><code>BackendTrafficPolicy</code> -&gt; <code>listIngressForBackendTrafficPolicy</code></li> <li><code>HTTPRoutePolicy</code> -&gt; <code>listIngressesByHTTPRoutePolicy</code></li> </ul></li> </ul> <p><strong>使用场景</strong>:</p> <ul> <li>处理 Policy 的 targetRefs 被移除时的清理工作</li> <li>确保当资源不再被引用时,能正确更新相关配置</li> </ul> <h4 id="整体工作流程">整体工作流程</h4> <pre><code>1. 事件触发 → 2. Predicate 过滤 → 3. MapFunc 映射 → 4. Reconcile 队列 → 5. Reconcile 执行 </code></pre> <h4 id="reconcile-主要步骤">Reconcile 主要步骤:</h4> <ol> <li><strong>获取 Ingress 对象</strong>:如果不存在则执行删除逻辑</li> <li><strong>查找 IngressClass</strong>:确定配置来源</li> <li><strong>处理 IngressClass Parameters</strong>:加载 GatewayProxy 配置</li> <li><strong>处理 TLS</strong>:加载证书 Secret</li> <li><strong>处理 Backends</strong>:加载 Service 和 EndpointSlice</li> <li><strong>处理 HTTPRoutePolicy</strong>:应用路由策略</li> <li><strong>处理 BackendTrafficPolicy</strong>:应用后端流量策略</li> <li><strong>更新 APISIX 配置</strong>:通过 Provider 同步到 APISIX</li> <li><strong>更新状态</strong>:更新 Ingress 和相关资源的状态</li> </ol> <h4 id="关键设计特点">关键设计特点</h4> <ol> <li><strong>索引优化</strong>:使用 Field Indexer 快速查找资源关系</li> <li><strong>级联更新</strong>:依赖资源变化时自动触发主资源更新</li> <li><strong>去重机制</strong>:避免重复处理同一个 Ingress</li> <li><strong>事件通道</strong>:使用 genericEvent channel 处理复杂的清理场景</li> <li><strong>条件过滤</strong>:通过 Predicate 减少不必要的 Reconcile</li> </ol> <h4 id="indexer性能优化">indexer性能优化</h4> <p>可以看到在上面的watch逻辑中有很多的list操作, 比如listIngressForBackendTrafficPolicy, 这个时候就需要事先在k8s client的indexer中建立索引来优化查询速度, 避免list全扫数据:</p> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="kd">func</span> <span class="nx">setupIngressIndexer</span><span class="p">(</span><span class="nx">mgr</span> <span class="nx">ctrl</span><span class="p">.</span><span class="nx">Manager</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> <span class="c1">// create IngressClass index </span><span class="c1"></span> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">mgr</span><span class="p">.</span><span class="nx">GetFieldIndexer</span><span class="p">().</span><span class="nx">IndexField</span><span class="p">(</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Background</span><span class="p">(),</span> <span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">Ingress</span><span class="p">{},</span> <span class="nx">IngressClassRef</span><span class="p">,</span> <span class="nx">IngressClassRefIndexFunc</span><span class="p">,</span> <span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</span> <span class="c1">// create Service index for quick lookup of Ingresses using specific services </span><span class="c1"></span> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">mgr</span><span class="p">.</span><span class="nx">GetFieldIndexer</span><span class="p">().</span><span class="nx">IndexField</span><span class="p">(</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Background</span><span class="p">(),</span> <span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">Ingress</span><span class="p">{},</span> <span class="nx">ServiceIndexRef</span><span class="p">,</span> <span class="nx">IngressServiceIndexFunc</span><span class="p">,</span> <span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</span> <span class="c1">// create secret index for TLS </span><span class="c1"></span> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">mgr</span><span class="p">.</span><span class="nx">GetFieldIndexer</span><span class="p">().</span><span class="nx">IndexField</span><span class="p">(</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Background</span><span class="p">(),</span> <span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">Ingress</span><span class="p">{},</span> <span class="nx">SecretIndexRef</span><span class="p">,</span> <span class="nx">IngressSecretIndexFunc</span><span class="p">,</span> <span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</span> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">mgr</span><span class="p">.</span><span class="nx">GetFieldIndexer</span><span class="p">().</span><span class="nx">IndexField</span><span class="p">(</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Background</span><span class="p">(),</span> <span class="o">&amp;</span><span class="nx">networkingv1</span><span class="p">.</span><span class="nx">Ingress</span><span class="p">{},</span> <span class="nx">TLSHostIndexRef</span><span class="p">,</span> <span class="nx">IngressTLSHostIndexFunc</span><span class="p">,</span> <span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span></code></pre></div> <h5 id="索引概览表">📊 <strong>索引概览表</strong></h5> <table> <thead> <tr> <th>索引名称</th> <th>索引字段</th> <th>索引函数</th> <th>主要用途</th> </tr> </thead> <tbody> <tr> <td>IngressClassRef</td> <td><code>ingressClassRef</code></td> <td><code>IngressClassRefIndexFunc</code></td> <td>根据 IngressClass 查找 Ingress</td> </tr> <tr> <td>ServiceIndexRef</td> <td><code>serviceRefs</code></td> <td><code>IngressServiceIndexFunc</code></td> <td>根据后端 Service 查找 Ingress</td> </tr> <tr> <td>SecretIndexRef</td> <td><code>secretRefs</code></td> <td><code>IngressSecretIndexFunc</code></td> <td>根据 TLS Secret 查找 Ingress</td> </tr> <tr> <td>TLSHostIndexRef</td> <td><code>tlsHostRefs</code></td> <td><code>IngressTLSHostIndexFunc</code></td> <td>根据 TLS 主机名查找 Ingress</td> </tr> </tbody> </table> <p>Ingress 的 4 个索引形成了一个完整的查询体系:</p> <ol> <li><strong>IngressClassRef</strong>:管理层面 - 按控制器分组</li> <li><strong>ServiceIndexRef</strong>:数据平面 - 后端服务关联</li> <li><strong>SecretIndexRef</strong>:安全层面 - TLS 证书管理</li> <li><strong>TLSHostIndexRef</strong>:域名层面 - SSL 配置管理</li> </ol> <p>这些索引确保了当任何依赖资源变化时,控制器都能<strong>快速、准确</strong>地找到需要更新的 Ingress,实现<strong>高效的级联更新</strong>和<strong>实时配置同步</strong>。</p> <h3 id="3-pingsix-ingress-controller架构决策">3. pingsix-ingress-controller架构决策</h3> <p>从apisix-ingress-controller的架构中, 我们知道apisix抽象了一种ADC(API Declarative CLI)的资源类型用来在ingress与apisix资源之间作为中间的桥梁, 如果直接使用apisix-ingress-controller来对接pingsix的话, 就需要pingsix完整的实现apisix的admin api, 并且还需要在使用时启动etcd.</p> <p>在我的印象中曾经看到过apisix-ingress-controller实现过一个不需要etcd的方案:</p> <ul> <li><a href="https://apisix.apache.org/blog/2023/10/18/ingress-apisix/">Embrace the Lightweight APISIX Ingress Controller Without etcd Dependency</a></li> </ul> <p>然后我在apisix-ingress-controller 1.8.x 版本的代码下找到了这个这个实现方式, 只是当前的 2.0.x 版本在引入了ADC相关的功能后去掉了<a href="https://github.com/api7/etcd-adapter">etcd-adapter</a>, 那我在考虑实现我的pingsix-ingress-controller时, 为了避免直接改动pingsix的代码, 并且也不希望在ingress启动时依赖etcd, 所以决定重新引入etcd-adapter, 然后为了后续pingsix-ingress-controller能同步apisix-ingress-controller的上游更新, 我决定在现有的ADC Executor的接口的基础上, 实现pingsix的Executor, 这样就可以在避免直接修改apisix的逻辑代码, 使用一种adapter的方式来实现我们自己的pingsix-ingress-controller.</p> <p><img src="https://github.com/user-attachments/assets/a10a7162-ea7d-4407-b7a6-b5481823592b" alt="Image2" width="700px" /></p> <p>可以看到我们实现的这个Executor, 其实就是重复了一遍ADC http serve的逻辑, 但是最终资源的数据是写入到etcd-adapter中的, 我们实现了一个apisix-ingress-controller最底层的抽象, 通过这种低成本的改造我们后续还可以继续同步apisix-ingress-controller的变更, 并不会造成代码冲突.</p> <h3 id="总结">总结</h3> <p>在实现<a href="https://github.com/zhu327/pingsix-ingress-controller">pingsix-ingress-controller</a>的过程中, 我完整的阅读了apisix-ingress-controller的代码, 收获了一些写operator的技巧, 然后在分析现有apisix-ingress-controller的代码架构时, 决定通过扩展ADC Executor的实现来实践了面向对象的开闭原则, 对于后续的代码更新合并开了一个好头.</p> 编写可维护的代码 -- 面向AI编程 https://zhu327.github.io/2025/10/16/%E7%BC%96%E5%86%99%E5%8F%AF%E7%BB%B4%E6%8A%A4%E7%9A%84%E4%BB%A3%E7%A0%81----%E9%9D%A2%E5%90%91ai%E7%BC%96%E7%A8%8B/ Thu, 16 Oct 2025 10:00:52 +0800 https://zhu327.github.io/2025/10/16/%E7%BC%96%E5%86%99%E5%8F%AF%E7%BB%B4%E6%8A%A4%E7%9A%84%E4%BB%A3%E7%A0%81----%E9%9D%A2%E5%90%91ai%E7%BC%96%E7%A8%8B/ <p>这是最近在小组内做的一次技术分享的文字稿,总体来说我觉得没有表达出特别硬核的内容,我本身也希望说这次分享不用讲的太硬核,在分享的开头还设计了互动环节,分享的过程中也尝试加入一些问答来互动,奈何现公司的技术氛围确实比较沉闷,似乎同事都比较I,好在最后的问答环节有几个比较好的问题,总体来说还算是一次成功的分享吧,希望自己能输出更多的好内容。</p> <h4 id="p1-我们写的代码是写给谁看的"><strong>P1: 我们写的代码是写给谁看的?</strong></h4> <ul> <li><strong>(🎤 互动环节):</strong> “大家有没有过接手一个项目时,看到的第一感觉是:我该从哪下手?结果花了三天才搞明白一个功能的逻辑。有过类似经历的兄弟举个手我看看?”</li> <li>小说家写小说给读者,建筑师盖房子给业主&hellip; 那我们程序员写代码,最终是写给未来的自己,和接手我们代码的同事看的。</li> <li>代码的可维护性,决定了我们是在“创造价值”还是在“创造负债”。</li> </ul> <h4 id="p2-举几个代码坏味道的例子"><strong>P2: 举几个代码坏味道的例子?</strong></h4> <ul> <li><strong>(🎤 互动环节):</strong> “看过《重构》的同学应该都知道代码坏味道。来,大家一起吐槽一下,你在项目里见过最痛苦的代码坏味道是什么?” (引导大家说出几个)</li> <li>其实按照我朴素的理解,能让我快速理清逻辑的就是好代码,理解起来很费劲的,那肯定就充满坏味道。</li> <li>正如我在入职后接手的<code>kitam</code>项目,它就充满了这些味道: <ul> <li><strong>代码逻辑不内聚</strong>:功能实现像天女散花。</li> <li><strong>分层过多且混乱</strong>:一个请求要穿越重重关卡。</li> <li><strong>外部依赖离散</strong>:数据库、缓存的调用没有统一规范。</li> </ul></li> </ul> <p></p> <h4 id="p3-今天分享的主题"><strong>P3: 今天分享的主题</strong></h4> <ul> <li>今天分享的主题是 编写可维护的代码,以及我们可探讨下面向AI编程。</li> <li>面对代码的坏味道和混乱,我们有什么良药?—— 答案是:一个好的架构。今天我将分享我们<code>kfinops</code>项目中使用的架构:整洁架构 (Clean Architecture)。</li> </ul> <h4 id="p4-我们的敌人是谁-是-耦合-这头怪兽"><strong>P4: 我们的敌人是谁?——是“耦合”这头怪兽!</strong></h4> <p><img src="https://github.com/user-attachments/assets/67458592-6307-44ed-a991-a6def7f53bf8" alt="Image3" width="700px" /></p> <ul> <li><strong>(🖼️ 视觉元素占位符: 此处放置一张生动的图片,比如一个身上贴满“耦合”标签的小怪兽 🐲)</strong></li> <li>在深入架构之前,我们先要认清我们真正的敌人:<strong>耦合 (Coupling)</strong>。</li> <li>什么是耦合?当你修改代码的一个地方,却意外地导致另一个看似无关的地方也必须修改,甚至直接崩溃时,你就遇到了耦合。</li> <li>整洁架构的核心目标只有一个:斩断不必要的耦合,为我们的代码建立“防火墙”,控制变更的“爆炸半径”。</li> <li>接下来,我会通过4个我们都经历过的惨痛问题,来展示整洁架构是如何一步步帮我们驯服这头怪兽的。</li> <li><strong>(🖼️ 视觉元素占位符: 此处展示完整的整洁架构“洋葱图”,并快速解释四个环:Entities -&gt; Use Cases -&gt; Adapters -&gt; Frameworks)</strong></li> </ul> <p><img src="https://github.com/user-attachments/assets/fb504fdb-4ba4-4dc8-8d01-c869da9be03d" alt="Image3" width="700px" /></p> <h4 id="p5-问题一-我只想改个业务规则-为什么还要动数据库和api的代码"><strong>P5: 问题一:“我只想改个业务规则,为什么还要动数据库和API的代码?”</strong></h4> <ul> <li><strong>(🖼️ 视觉元素占位符: 洋葱图高亮最内层的 &ldquo;Entities&rdquo; 环)</strong></li> <li><p><strong>痛点场景:</strong> 你想修改一个实体(比如 <code>User</code>)的业务规则。但在很多项目中,这个实体被定义成了一个“万能Struct”:</p> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="c1">// user.go </span><span class="c1">// 它既是业务实体,又是GORM模型,还是HTTP响应体 </span><span class="c1"></span><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> <span class="nx">ID</span> <span class="kt">uint</span> <span class="s">`json:&#34;id&#34; gorm:&#34;primaryKey&#34;`</span> <span class="c1">// &lt;-- 高亮此行 </span><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`json:&#34;name&#34; gorm:&#34;size:255&#34;`</span> <span class="nx">Points</span> <span class="kt">int</span> <span class="s">`json:&#34;points&#34;`</span> <span class="c1">// ... </span><span class="c1"></span><span class="p">}</span></code></pre></div> <ul> <li>现在,产品说:为了兼容新App,<code>ID</code> 字段对外要改成 <code>user_id</code>。</li> <li>你只能修改:<code>ID uint \</code>json:&ldquo;user_id&rdquo; gorm:&ldquo;primaryKey&rdquo;``。</li> <li><strong>问题来了:</strong> 你只是在修改一个前端展示字段的名称,却迫使你修改了一个核心业务实体(User)的文件。<strong>这就是业务逻辑和展示细节的耦合。</strong></li> </ul></li> <li><p><strong>解药:一个纯净的【领域层 Domain Layer】</strong></p> <ul> <li><strong>它的唯一职责</strong>:建立一个“无菌室”,只存放最核心、最纯粹的业务实体和业务规则。</li> <li><p><strong><code>kfinops</code> 的实践:</strong></p> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="c1">// internal/domain/subdomain.go </span><span class="c1">// 只有业务属性,没有json,gorm等任何技术标签 </span><span class="c1"></span><span class="kd">type</span> <span class="nx">SubDomain</span> <span class="kd">struct</span> <span class="p">{</span> <span class="nx">ID</span> <span class="kt">string</span> <span class="c1">// 纯粹的业务ID </span><span class="c1"></span> <span class="nx">FullDomain</span> <span class="kt">string</span> <span class="nx">Status</span> <span class="nx">SubDomainStatus</span> <span class="p">}</span></code></pre></div></li> <li><p><strong>效果:</strong> 业务核心完全稳定。前端/数据库的任何变化,都不会触碰到这个文件。</p></li> </ul></li> </ul> <h4 id="p6-问题二-创建子域名-这个功能-代码到底在哪"><strong>P6: 问题二:“‘创建子域名’这个功能,代码到底在哪?”</strong></h4> <ul> <li><strong>(🖼️ 视觉元素占位符: 洋葱图高亮第二层的 &ldquo;Use Cases&rdquo; 环)</strong></li> <li><strong>(🎤 互动环节):</strong> “举个手,谁遇到过想看懂一个功能,结果在IDE里跳了十几个文件的情况?”</li> <li><strong>痛点场景:</strong> 业务流程像天女散花,心智负担极大,你根本找不到一个地方能看清这个功能的“剧本”。<strong>这就是业务流程的逻辑离散。</strong></li> <li><p><strong>解药:一个清晰的【用例层 Use Case Layer】</strong></p> <ul> <li><strong>它的唯一职责</strong>:像一个“导演”,负责编排一个完整的业务故事。一个Use Case就代表系统的一个能力。</li> <li><p><strong><code>kfinops</code> 的实践:</strong></p> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="c1">// in internal/usecase/subdomain/service.go </span><span class="c1"></span><span class="kd">type</span> <span class="nx">SubDomainUseCase</span> <span class="kd">struct</span> <span class="p">{</span> <span class="nx">repo</span> <span class="nx">repository</span><span class="p">.</span><span class="nx">SubDomainRepository</span> <span class="c1">// 依赖接口 </span><span class="c1"></span><span class="p">}</span> <span class="kd">func</span> <span class="p">(</span><span class="nx">uc</span> <span class="o">*</span><span class="nx">SubDomainUseCase</span><span class="p">)</span> <span class="nx">CreateSubDomain</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> <span class="c1">// 1. 验证输入 (DTO) &lt;-- 高亮 </span><span class="c1"></span> <span class="c1">// 2. 创建 Domain 实体 &lt;-- 高亮 </span><span class="c1"></span> <span class="c1">// 3. 调用 Domain 实体业务方法 &lt;-- 高亮 </span><span class="c1"></span> <span class="c1">// 4. 通过【接口】进行持久化 &lt;-- 高亮 </span><span class="c1"></span> <span class="k">return</span> <span class="nx">uc</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nx">Save</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">domainEntity</span><span class="p">)</span> <span class="p">}</span></code></pre></div></li> <li><p><strong>效果:</strong> 想理解一个功能,只需要看这一个文件。它清晰地讲述了“做什么(What)”,而不关心“怎么做(How)”。</p></li> </ul></li> </ul> <h4 id="p7-问题三-数据库要从mysql换成mongodb-完了-项目要重写了"><strong>P7: 问题三:“数据库要从MySQL换成MongoDB,完了,项目要重写了!”</strong></h4> <ul> <li><strong>(🖼️ 视觉元素占位符: 洋葱图高亮第三层的 &ldquo;Adapters&rdquo; 环,并画一个箭头表示依赖反转)</strong></li> <li><strong>痛点场景:</strong> 你的代码里到处都是 <code>gorm.DB.Where(...).Find(...)</code>。你的业务逻辑“知道”你正在使用GORM和MySQL。<strong>这就是业务逻辑和数据存储技术的强耦合。</strong></li> <li><strong>解药:【适配器层 Adapter】 + 【依赖倒置原则】</strong> <ul> <li><strong>它的唯一职责</strong>:将技术细节“适配”成业务层能理解的“插件”。</li> <li><strong>核心魔法——依赖倒置</strong>: <ol> <li><strong>Use Case(使用者)</strong> 在自己包里定义接口。</li> <li><strong>Adapter(实现者)</strong> 去实现这个接口。</li> </ol></li> <li><strong>效果:</strong> 技术实现变成了可插拔的“零件”。更换数据库?只需要写一个新的Repository实现,然后在DI配置里换掉即可,<strong>Use Case层的代码一行都不用改!</strong></li> </ul></li> </ul> <h4 id="p8-问题四-这代码根本没法写单元测试"><strong>P8: 问题四:“这代码根本没法写单元测试!”</strong></h4> <p><img src="https://github.com/user-attachments/assets/7271b7b0-f51e-40ce-8fe0-dfad4850ed7e" alt="Image3" width="700px" /></p> <ul> <li><strong>(🖼️ 视觉元素占位符: 一张图片,左边是混乱的毛线球代表紧耦合,右边是整齐的乐高积木代表可测试)</strong></li> <li><strong>(🎤 互动环节):</strong> “诚实地举手,谁因为代码太难测试而放弃写单元测试的?”</li> <li><strong>痛点场景:</strong> 业务逻辑里 <code>new</code> 了一个数据库连接,导致单元测试必须依赖真实环境。<strong>这是【可测试性灾难】。</strong></li> <li><p><strong>解药:【接口】 + 【依赖注入 Dependency Injection】</strong></p> <ul> <li><strong>它的唯一职责</strong>:将组件之间的依赖关系从“硬编码”变为“外部配置”。</li> <li><p><strong>效果:在单元测试中,我们可以轻松地“骗”过 Use Case:</strong></p> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="c1">// 我们可以轻易地用一个 &#34;假的&#34; mockRepo 替换掉 &#34;真的&#34; GormRepo </span><span class="c1"></span><span class="nx">mockRepo</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="nx">MockSubDomainRepository</span><span class="p">)</span> <span class="c1">// 告诉 mockRepo: &#34;当有人调用Save方法时,假装成功&#34; </span><span class="c1"></span><span class="nx">mockRepo</span><span class="p">.</span><span class="nx">On</span><span class="p">(</span><span class="s">&#34;Save&#34;</span><span class="p">,</span> <span class="nx">mock</span><span class="p">.</span><span class="nx">Anything</span><span class="p">,</span> <span class="nx">mock</span><span class="p">.</span><span class="nx">Anything</span><span class="p">).</span><span class="nx">Return</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span> <span class="c1">// &lt;-- 高亮 </span><span class="c1"></span> <span class="nx">useCase</span> <span class="o">:=</span> <span class="nx">NewSubDomainUseCase</span><span class="p">(</span><span class="nx">mockRepo</span><span class="p">)</span> <span class="c1">// 依赖被注入了! &lt;-- 高亮 </span><span class="c1"></span><span class="nx">err</span> <span class="o">:=</span> <span class="nx">useCase</span><span class="p">.</span><span class="nx">CreateSubDomain</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> <span class="o">//</span> <span class="nx">测试通过</span><span class="err">!</span><span class="nx">全程没有碰过数据库</span><span class="err">!</span></code></pre></div></li> <li><p><strong>这让编写快速、可靠的单元测试成为可能,极大地保证了代码质量。</strong></p></li> </ul></li> </ul> <h4 id="p9-可维护性的延伸-构建强大的可观测性"><strong>P9: 可维护性的延伸:构建强大的可观测性</strong></h4> <p><img src="https://github.com/user-attachments/assets/297e1818-ea6e-4a33-8747-5ad78e004e40" alt="Image3" width="700px" /></p> <ul> <li>作为SRE团队,我们不仅要写出可维护的代码,更要确保系统在生产环境中是‘可理解’、‘可诊断’的。</li> <li><strong>可观测性三大支柱:</strong> <ul> <li><strong>🪵 日志 (Logging) - 定位“哪里”出错了</strong>: 接入KAE,结构化日志,包含<code>request_id</code>。</li> <li><strong>📊 指标 (Metrics) - 了解“怎么样”了</strong>: Prometheus + Grafana Dashboard。</li> <li><strong>🔗 追踪 (Tracing) - 分析“为什么”慢了</strong>: OpenTelemetry 分布式追踪。</li> </ul></li> <li><strong>小结:整洁的架构让代码在“静态时”易于理解;完善的可观测性体系则让系统在“运行时”易于诊断。</strong></li> </ul> <h4 id="p10-面向ai编程-架构即契约-约束即指导"><strong>P10: 面向AI编程:架构即契约,约束即指导</strong></h4> <p><img src="https://github.com/user-attachments/assets/640a4f7e-5ef3-4ec6-806e-5e72fd31cb26" alt="Image3" width="700px" /></p> <ul> <li><strong>(🖼️ 视觉元素占位符: 一个对比图)</strong> <ul> <li><strong>左边 (无架构):</strong> 人类 🤯 → prompt → AI 🤖 → 一堆散乱代码 💩</li> <li><strong>右边 (有架构):</strong> 人类 😎 → 短prompt → AI 🤖 → 精准、分层的代码 ✅</li> </ul></li> <li><strong>一个好的架构,就是整个项目的“代码契约 (Code Contract)”</strong>,它为AI提供了一个清晰、明确的上下文环境。</li> <li><strong>有了这份“契约”,约束就变成了对AI最有效的指导:</strong> <ol> <li><strong>架构定义了“剧本 (Playbook)”</strong>: AI会像一个熟悉项目的老手一样,在正确的地方写代码。</li> <li><strong>约束提供了“护栏 (Guardrails)”</strong>: AI不会在usecase层直接写SQL。</li> <li><strong>“契约”简化了我们的“指令 (Prompt)”</strong>: 从“一步步告诉AI怎么做”,变成了“<strong>为 <code>CreateUser</code> 用例实现API</strong>”这样简单的指令。</li> </ol></li> <li><strong>结论:架构即提示 (Architecture as the Prompt)。</strong></li> </ul> <h4 id="p11-总结与行动指南"><strong>P11: 总结与行动指南</strong></h4> <ul> <li>编写可维护代码的核心是“降低理解成本”和“控制变更影响”。</li> <li>整洁架构是一份强大的“代码契约”,通过强制规则保证了项目的一致性。</li> <li><strong>(🚀 行动号召):</strong> <ul> <li><strong>团队可以马上尝试的实践</strong>: 在下一个新功能里,先和同事花10分钟讨论一下它的 Use Case 是什么。</li> <li><strong>推荐大家试用</strong>: 克隆我们的 <code>kfinops</code> 项目或<code>go-clean-arch</code>模板,亲手运行一下,感受分层的清晰。</li> </ul></li> <li><strong>面向AI编程不是一句口号,而是一种新的工作范式。</strong> 你的架构越清晰、约束越明确,AI就越能成为你的得力助手。</li> </ul> <h4 id="p12-q-a"><strong>P12: Q&amp;A</strong></h4> <ul> <li><strong>谢谢大家!</strong></li> <li><strong>项目模板参考: <a href="https://github.com/zhu327/go-clean-arch">https://github.com/zhu327/go-clean-arch</a></strong></li> <li><strong>大家有什么问题吗?或者对我们团队未来的代码规范有什么建议?</strong></li> </ul> Vibe Coding有感: 从PingSIX重构说起 https://zhu327.github.io/2025/09/02/vibe-coding%E6%9C%89%E6%84%9F-%E4%BB%8Epingsix%E9%87%8D%E6%9E%84%E8%AF%B4%E8%B5%B7/ Tue, 02 Sep 2025 14:55:52 +0800 https://zhu327.github.io/2025/09/02/vibe-coding%E6%9C%89%E6%84%9F-%E4%BB%8Epingsix%E9%87%8D%E6%9E%84%E8%AF%B4%E8%B5%B7/ <p>说到“Vibe Coding”,这个词最近在开发者圈子里越来越流行。在过去,我的实践很大程度上还停留在比较初级的阶段:把需求和代码片段在 ChatGPT、Google AI Studio 或是 Grok 之间来回地复制粘贴。来到新公司后,虽然开通了 GitHub Copilot,体验有所提升,但感觉它更多时候还是一个“超级智能补全”工具。直到今年7月份,公司给配上了 Cursor,我的编程体验才真正开启了一场变革,正式进入了 Vibe Coding 的奇妙旅程。</p> <p>恰好,距离我上次更新个人项目 <a href="https://github.com/zhu327/pingsix">PingSIX</a> 已经有一段时间了。PingSIX 是一个我基于 Cloudflare 高性能框架 Pingora 开发的 API 网关。两周前,Pingora 发布了 0.6.0 版本,这给了我一个绝佳的契机。我决定利用这个机会,彻底实践一次全程使用 Cursor 的 Vibe Coding,对 PingSIX 进行一次大重构。</p> <p>这次重构断断续续花了一周时间,最终新增了 6636 行代码,删除了 3578 行。这不仅仅是数字上的变化,整个项目在架构、可维护性、可读性、安全性及性能上,都达到了一个我自己非常满意的状态。</p> <p></p> <h3 id="我的-vibe-coding-工作流">我的 Vibe Coding 工作流</h3> <p>在这次重构之旅中,我逐渐摸索出了一套与 AI 高效协作的工作流。这套流程的核心思想是:<strong>你,作为程序员,依然是掌舵者;而 AI,是你最强大的副驾驶。</strong></p> <h4 id="1-选择对的-副驾-模型很重要">1. 选择对的“副驾”:模型很重要</h4> <p>工欲善其事,必先利其器。我尝试了 Cursor 内置的多个模型,最终发现 <strong>claude-4-sonnet</strong> 是我的首选。它表现最稳定,理解复杂上下文的能力很强,生成代码的质量也相当高,而且价格合适。其它模型或多或少会有些小问题,而 Sonnet 则提供了一种“无脑用”的稳定预期。</p> <h4 id="2-精准的指令-上下文工程是关键">2. 精准的指令:上下文工程是关键</h4> <p>把 AI 当成一个需要明确指令的初级程序员。在你让 Cursor 写代码前,<strong>必须先自己整理好需求</strong>。需求描述得越详细、越清晰,AI 生成的代码质量就越高。我会把相关的代码文件、需要调用的函数定义,都通过 <code>@</code> 符号添加到上下文中,为 AI 提供一个完整的“作战地图”。</p> <h4 id="3-从战略到战术-让-ai-先当-架构师">3. 从战略到战术:让 AI 先当“架构师”</h4> <p>如果你自己对某个功能也一头雾水,不知道该如何下手,那就先别急着让 AI 写代码。我会先打开 “Ask” 模式,把我的难题抛给它,让 LLM 帮我出谋划策。通过一遍遍的追问和迭代,更新我的需求,最终将 AI 提供的方案整理成一份详尽的文档,甚至细化到某个具体逻辑的实现步骤。有时,我还会让它<strong>先为我的设想编写单元测试</strong>,然后再让 Agent 遵循这份“蓝图”去实现功能代码。</p> <h4 id="4-你永远是代码的最终责任人">4. 你永远是代码的最终责任人</h4> <p>要时刻记住,LLM 的回答具有不确定性。它是一个强大的工具,但不能替代你的专业判断。我的原则是:<strong>先由我来确定“怎么写”的顶层设计,再让 LLM 来完成“体力活”</strong>。当代码生成后,<strong>Code Review 是必不可少的一环</strong>。仔细审查每一行代码,确保它完全符合你的预期。如果不确定,那就让 LLM 继续为这段代码补充单元测试,用测试用例来验证逻辑的正确性。</p> <h4 id="5-让-ai-成为你的-代码评审专家">5. 让 AI 成为你的“代码评审专家”</h4> <p>除了写代码,AI 在 Code Review 方面也表现出色。有时我们自己很难发现项目中的潜在问题,这时就可以借助 LLM 的视角。我会用下面这个 Prompt 来请求一次全面的“代码体检”:</p> <pre><code>你是一个Rust编程大师, API网关领域的专家, 请你从代码总体的架构, 可读性, 可维护性, 安全性, 性能方面全面的review我的项目代码, 给出你的分析报告, 并对有哪些代码需要修改给出具体的修改建议。 </code></pre> <h3 id="从局部优化到全局审视-当ai助手遇到架构难题">从局部优化到全局审视:当AI助手遇到架构难题</h3> <p>在使用上述 Prompt 的过程中,我发现了一个 Cursor 的局限性。它的文件索引方式似乎是基于代码片段的向量化,这导致它在进行 Review 时,非常擅长发现局部的、函数级别的问题,但<strong>很难从项目整体的架构、分层和依赖关系上给出宏观的分析</strong>。</p> <p>为了解决这个问题,我引入了两个外部工具,形成了一套组合拳:</p> <ol> <li><strong><a href="https://gitingest.com">gitingest.com</a></strong>: 这个小工具能把整个项目的代码,按照文件结构,完整地生成一个 Markdown 文本。</li> <li><strong><a href="https://aistudio.google.com">aistudio.google.com</a></strong>: 我使用的是 Gemini 2.5 Pro 模型,它支持百万级别的 Token 上下文,足以一次性“吞下”我的整个项目代码。</li> </ol> <p>我的工作流变成了:使用 <code>gitingest</code> 打包项目 -&gt; 将生成的 Markdown 完整地粘贴到 Google AI Studio -&gt; 使用上面那个专家 Prompt 让 Gemini 进行全局审查。</p> <p>经过这个流程,我得到了一份详尽的“体检报告”,其深度和广度甚至超出了我的预期。下面是最近一次 Gemini Review 报告的一部分摘录:</p> <blockquote> <p>好的,作为一名在 Rust 编程和 API 网关领域都有深入研究的专家,我非常荣幸能为您审查 PingSIX 这个项目。</p> <p>首先,请允许我表达我的赞赏:<strong>这是一个非常出色、设计精良的项目</strong>。它不仅基于 Cloudflare Pingora 这一高性能框架,还在架构设计、代码实现和功能完整性上达到了很高的水準。许多设计决策,例如配置热加载的实现方式、插件系统的设计以及对安全性的关注,都体现了作者深厚的技术功底和对 API 网关领域的深刻理解。</p> <p>以下是我的综合分析报告,涵盖了总体架构、可读性、可维护性、安全性、性能以及具体的修改建议。</p> <hr /> <h4 id="1-总体架构-overall-architecture">1. 总体架构 (Overall Architecture)</h4> <p><strong>分析:</strong> 项目的整体架构是现代、模块化且高度可扩展的,非常符合云原生 API 网关的设计理念。</p> <ul> <li><strong>坚实的基础</strong>: 项目选择基于 Cloudflare 的 Pingora 框架,这是一个明智的决定。Pingora 提供了经过生产环境严酷考验的高性能、多线程的异步 I/O 核心,让项目可以专注于网关业务逻辑的实现,而不是底层网络细节。</li> <li><strong>配置驱动</strong>: &hellip; 特别是 Etcd 的集成,通过 <code>arc-swap</code> 和 <code>dashmap</code> 实现的配置热加载机制,是高性能网关的黄金标准&hellip;</li> <li><strong>逻辑分层清晰</strong>: 项目代码在 <code>src</code> 目录下按功能清晰地划分了模块(<code>config</code>, <code>proxy</code>, <code>plugin</code>, <code>admin</code>, <code>core</code> 等),职责分明。</li> <li><strong>共享服务设计</strong>: <code>SharedHealthCheckService</code> 的设计是一个关键的性能优化&hellip;</li> </ul> <p><strong>评价:</strong> 架构设计堪称典范,无论是性能、可扩展性还是可靠性方面都考虑得非常周到,达到了业界领先水平。</p> <hr /> <h4 id="2-代码可读性-readability">2. 代码可读性 (Readability)</h4> <p><strong>分析:</strong> * <strong>命名规范</strong>: &hellip; * <strong>注释质量高</strong>: &hellip;更重要的是解释了“为什么这么做”(The Why)&hellip; * <strong>错误处理</strong>: &hellip;<code>ProxyError</code> 枚举类型非常完善&hellip;</p> <p><strong>评价:</strong> 代码可读性极佳。</p> <hr /> <h4 id="3-可维护性-maintainability">3. 可维护性 (Maintainability)</h4> <p><strong>分析:</strong> * <strong>高内聚,低耦合</strong>: &hellip; * <strong>强大的配置校验</strong>: &hellip;在 <code>src/config/mod.rs</code> 中大量使用了 <code>validator</code> crate&hellip; * <strong>泛型编程的极致运用</strong>: <code>src/admin/mod.rs</code> 中的 <code>AdminResource</code> Trait 和 <code>ResourceHandler&lt;T&gt;</code> 泛型结构是可维护性的典范&hellip;</p> <p><strong>评价:</strong> 项目的可维护性非常出色。</p> <hr /> <h4 id="4-安全性-security">4. 安全性 (Security)</h4> <p><strong>分析:</strong> * <strong>容器安全</strong>: <code>Dockerfile</code> 遵循了最佳实践,使用了多阶段构建&hellip;并且创建了一个低权限的非 root 用户&hellip; * <strong>API 认证</strong>: &hellip;在比较密钥时使用了 <code>constant_time_eq</code> 函数,有效防止了时序攻击(Timing Attack)。 * <strong>TLS 配置</strong>: &hellip;代码强制将 TLS 的最低版本设置为 TLS 1.3&hellip;</p> <p><strong>评价:</strong> 安全性设计考虑周全,遵循了行业内的安全最佳实践。</p> <hr /> <h4 id="5-性能-performance">5. 性能 (Performance)</h4> <p><strong>分析:</strong> * <strong>无锁热加载</strong>: 使用 <code>arc-swap</code> 来实现路由表&hellip;避免了读写锁带来的性能开销&hellip; * <strong>高效的路由匹配</strong>: &hellip;通过<strong>反转主机名</strong>来巧妙地利用 <code>matchit</code> 的前缀匹配能力实现对通配符域名&hellip;这是一个极其聪明和高效的实现。 * <strong>预编译与缓存</strong>: &hellip;</p> <p><strong>评价:</strong> 性能设计和优化做到了极致&hellip;<code>README.md</code> 中提到的性能指标(100K+ RPS, &lt;10ms P99 延迟)基于此架构是完全可信的。</p> <hr /> <h3 id="总结">总结</h3> <p>PingSIX 是一个非常优秀的项目,其代码质量、架构设计和性能考量都达到了专业甚至顶尖的水平。它不仅是一个功能强大的 API 网关,更是一个极佳的 Rust 工程实践范例。 &hellip; 希望这份详尽的审查报告对您有所帮助!</p> </blockquote> <p>拿到这份宏观报告后,我就可以把其中具体的修改建议,一段段地贴回 Cursor,让它帮我完成代码的微调和优化。改完一轮,再打包给 Gemini 重新审查,如此循环,直到臻于完美。</p> <h3 id="一点感想">一点感想</h3> <p>LLM Agent 的强大无疑极大地提升了我的开发效率,但它也像一把双刃剑。这种前所未有的效率有时也会让上游的 PM 或 Leader 觉得:“反正你很快就能做出来,那我们就快速地改需求吧。” 结果可能导致我们做了很多功能,但真正沉淀和落地的却不多,这或许是“高效”时代下新的烦恼。</p> <p>面对层出不穷的新工具和新范式,我的建议是:<strong>先用起来再说</strong>。用的多了,体会自然就深了。别人的经验可以参考,但更重要的是在实践中找到最适合自己的那套 Vibe。</p> <p>我不确定 LLM 最终会不会淘汰程序员这个职业,但我能确定的是,<strong>会 Vibe Coding 的程序员,一定会淘汰不会 Vibe Coding 的程序员</strong>。所以,朋友们,都 Vibe起来吧!</p> 整洁架构落地实践 https://zhu327.github.io/2025/08/04/%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5/ Mon, 04 Aug 2025 14:55:52 +0800 https://zhu327.github.io/2025/08/04/%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5/ <p>最近在新公司接手了一个历史悠久的项目,功能不多,代码量却不小。深入其中,才发现有太多太多的槽点,简直让人头大。每当要新增一个功能或者修复一个 Bug,都感觉像是在雷区里跳舞,步步惊心。</p> <p>总结下来,这个项目主要有这么几个“硬伤”:</p> <ol> <li><strong>代码逻辑不内聚</strong>:同一个功能的实现,像天女散花一样分散在好几个不同的文件里,想理清完整的逻辑链条,得在 IDE 里跳来跳去,极其耗费心智。</li> <li><strong>分层过多且混乱</strong>:一个简单的请求,要穿越重重关卡,经过好几个层级的调用才能抵达终点。有时你都分不清某一层到底是干嘛的,感觉纯粹是为了分层而分层。</li> <li><strong>内部自研框架不好用</strong>:项目依赖了一个内部的 <code>kgo</code> 框架,但文档缺失,设计理念也比较陈旧,出了问题排查起来非常困难。</li> <li><strong>外部依赖离散</strong>:对数据库、缓存、外部 API 的调用封装得五花八门,没有统一的规范和入口,整个项目像一个杂乱的“百宝箱”。</li> </ol> <p>恰好,Leader 对这个项目制定了新的方向与目标,之前这些遗留问题实实在在成了我们前进路上的绊脚石。因此,一次彻底的架构升级与重构势在必行。同时,部门内也正在推广项目规范化和最佳实践输出,这简直是天赐良机。</p> <p>很久以前我就读过《架构整洁之道》这本书,在之前的项目中也或多或少受到一些启发,但从未有机会从一个项目初始就完整地贯彻整洁架构的理念。这次,我决定抓住机会,来一场彻彻底底的整洁架构实践。在这个过程中,我感觉自己对整洁架构的理解,以及对 SOLID 原则的落地方式,都有了前所未有的深入体会。</p> <p></p> <p>在实践的过程中,我将心得与经验沉淀下来,开源了一个即开即用的 Go 整洁架构项目模板,希望能给有同样需求的同学一些参考: <strong><a href="https://github.com/zhu327/go-clean-arch">https://github.com/zhu327/go-clean-arch</a></strong></p> <h3 id="项目模板特性">项目模板特性</h3> <ul> <li><strong>Clean Architecture</strong>: 清晰地分离业务逻辑与基础设施。</li> <li><strong>Dependency Injection</strong>: 使用 Google Wire 实现编译时依赖注入,避免反射。</li> <li><strong>Structured Logging</strong>: 开箱即用的结构化日志。</li> <li><strong>Configuration Management</strong>: 基于 Viper 的环境化配置管理。</li> <li><strong>Docker Support</strong>: 包含 <code>Dockerfile</code> 和 <code>docker-compose.yaml</code>,便于部署。</li> </ul> <h3 id="架构设计">架构设计</h3> <p>这个项目严格遵循整洁架构(Clean Architecture)的原则,确保代码库的可扩展性、可维护性和可测试性。</p> <h4 id="各层描述">各层描述</h4> <ul> <li><strong>🔵 Domain 层</strong>: 包含核心的业务逻辑和实体。它是最独立的层,不依赖于任何其他层。</li> <li><strong>🟣 Use Case 层</strong>: 通过与 Domain 层交互来编排业务工作流。它定义了供 Adapter 层实现的接口。</li> <li><strong>🟠 Adapter 层</strong>: 作为与外部世界(如 UI、数据库、外部 API)沟通的桥梁。它实现了 Use Case 层定义的接口。</li> <li><strong>🟢 External World</strong>: 代表与应用程序交互的外部系统,如 Web 客户端、数据库或第三方服务。</li> </ul> <h4 id="核心原则">核心原则</h4> <ol> <li><strong>依赖方向</strong>: 所有依赖都必须指向内部。Domain 层位于中心,任何内层都不能依赖于外层。这是依赖倒置的核心。</li> <li><strong>接口隔离</strong>: 接口由消费者(Use Case 层)定义,由提供者(Adapter 层)实现。这使得业务逻辑与基础设施细节解耦。</li> <li><strong>分层隔离</strong>: 每一层只与它的相邻层交互,保持清晰的职责分离。</li> </ol> <h4 id="项目结构">项目结构</h4> <pre><code>internal/ ├── domain/ # Domain 层 (业务实体和规则) ├── usecase/ # Use Case 层 (业务逻辑, 接口, DTOs) ├── adapter/ # Adapter 层 │ ├── delivery/ # 交付机制 (例如, HTTP, gRPC handlers) │ ├── repository/ # 仓库实现 (数据库访问) │ └── gateway/ # 到外部服务的网关 └── di/ # 依赖注入配置 (Wire) </code></pre> <h3 id="实践中的思考与-顿悟">实践中的思考与“顿悟”</h3> <p>理论总是美好的,但实践才是检验真理的唯一标准。在重构过程中,我遇到了不少困惑,也收获了很多“原来如此”的顿悟时刻。</p> <h4 id="1-每一层到底应该放什么">1. 每一层到底应该放什么?</h4> <p>刚开始划分代码时,我经常会纠结一个结构体、一个文件到底该放在哪。经过反复的思考和试错,我总结出了一套相对清晰的指导方针。</p> <ul> <li><p><strong><code>internal/domain/</code></strong></p> <ul> <li><strong>✅ 应该包含</strong>: 核心业务实体(代表业务对象的 Struct)、值对象、领域服务接口、与领域相关的枚举和常量。</li> <li><strong>❌ 不应包含</strong>: HTTP 请求/响应结构体、分页或 API 特定数据等应用级概念、任何基础设施细节(如 JSON 标签)。</li> </ul></li> <li><p><strong><code>internal/usecase/</code></strong></p> <ul> <li><strong>✅ 应该包含</strong>: 具体业务用例的实现(如 <code>CreateUser</code>, <code>LoginUser</code>)、依赖项的接口定义(如 <code>UserRepository</code>)、请求和响应的 DTO(数据传输对象)。</li> </ul></li> <li><p><strong><code>internal/adapter/</code></strong></p> <ul> <li><strong>✅ 应该包含</strong>: 将请求转换为用例调用的 HTTP/gRPC 处理器、实现 Use Case 层接口的数据库仓库、外部服务的客户端(网关)。</li> </ul></li> </ul> <p>通过这个项目我深刻地体会到,<strong>项目的根本目的是解决现实世界的问题</strong>。我们需要对现实中的实体进行建模,这就是 <code>domain</code> 层的使命。这个实体必须纯粹,不依赖任何外部技术,只包含自身的业务规则和逻辑。</p> <p>而 <code>usecase</code> 层,则是针对领域模型的一次具体应用和落地。它需要依赖外部能力,比如查询一次数据库、调用一次 gRPC。这时,<strong>我们不应该直接在 <code>usecase</code> 中去 new 一个 DB client</strong>。而是应该在 <code>usecase</code> 中定义业务所需的 <code>interface</code>,然后由 <code>adapter</code> 层的 <code>repository</code> 或 <code>gateway</code> 来实现这些接口。</p> <p>这样一来,<code>repository</code> 就不再只是对 GORM 或 <code>sqlx</code> 的简单封装,它变成了 <code>usecase</code> 接口的具体“提供者”。它需要将从数据库查出的数据模型,转换为 <code>usecase</code> 能理解的 <code>domain</code> 模型。最后,通过依赖注入,实现了完美的依赖倒置。</p> <h4 id="2-关于依赖注入-从排斥到接受">2. 关于依赖注入:从排斥到接受</h4> <p>在以往的项目中,我基本没用过依赖注入,项目里充斥着各种 <code>init()</code> 函数和全局变量,给测试和维护带来了无尽的痛苦。后来接触到依赖注入,有个老哥自己撸了一套基于反射的 DI 框架,读他的代码真的太难受了,完全不知道依赖到底是怎么注入进来的,魔法感十足,这导致我对依赖注入一度有先入为主的排斥。</p> <p>但在实践整洁架构的过程中,由于 <code>usecase</code> 必须做依赖倒置,我不得不引入 DI 工具。最终选择了 Google 的 <code>wire</code>。用过之后才发现,<strong>它其实并没有什么魔法</strong>,只是通过扫描代码,自动生成了那些“手动挡”的初始化代码而已(<code>wire_gen.go</code>)。相对于通过反射实现的“自动挡”,这种代码生成的方式让一切依赖关系都变得明确和可知,可读性好太多了。</p> <h4 id="3-dto-的归属之争">3. DTO 的归属之争</h4> <p>这是一个让我纠结了很久的问题。<code>usecase</code> 理想情况下只应该依赖 <code>domain</code> 定义的模型。然而在实际业务中,总会有一些不属于 <code>domain</code> 核心模型的结构,比如 <code>CreateUserRequest</code>、<code>UserListResponse</code>。注意,这并非 <code>delivery</code> 层用于解析 JSON 的结构体,而是 <code>usecase</code> 自身执行业务逻辑所需要的数据结构。</p> <p>我曾经犹豫过,要不要把这些结构体也塞进 <code>domain</code> 层?</p> <p>纠结过后,我得出的结论是:<strong>不能!一定要保持 <code>domain</code> 层的纯粹性</strong>。这些结构体实际上是和某个具体的 <code>usecase</code> 强绑定的,它们应该属于 <code>usecase</code> 层。所以,我在 <code>usecase</code> 层下也创建了 <code>dto</code> 目录,用来存放这些用例专属的请求/响应结构。</p> <p>这确实会导致 <code>delivery</code> (HTTP) 层和 <code>usecase</code> 层可能都有各自的 DTO,初看可能会有些冗余和困惑。但这样做的好处是职责更清晰,<code>usecase</code> 不会因为 <code>delivery</code> 层的变化(比如修改一个 JSON 字段名)而被迫修改。这是为了层与层之间的解耦,必须付出的代价。</p> <h4 id="4-我终于悟了-在调用者包中定义接口">4. 我终于悟了:“在调用者包中定义接口”</h4> <p>我之前读到过 Go 社区的一个广为流传的建议(出自 <a href="https://colobu.com/gotips/018.html">colobu.com/gotips/018.html</a>):</p> <blockquote> <p><strong>在使用者的包中定义接口,而不是提供者的包中定义。</strong></p> </blockquote> <p>说实话,在没有深入实践整洁架构之前,我对这条原则一直是一知半解。为什么接口要让“用的人”来定义,而不是“做的人”来定义呢?</p> <p>在这次重构之后,我“悟了”。<strong>这不就是对“依赖倒置原则”最精准、最通俗的诠释吗!</strong></p> <p>在我们的架构里: - <code>usecase</code> 是接口的<strong>使用者</strong>(消费者)。 - <code>adapter</code> 是接口的<strong>实现者</strong>(提供者)。</p> <p><code>usecase</code> 说:“我需要一个能根据用户 ID 找到用户,并返回 <code>domain.User</code> 的能力,我不管你是从 MySQL、Redis 还是从文件中获取,总之,你得满足我定义的这个 <code>UserRepository</code> 接口。”</p> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="c1">// in usecase/iface/repository.go </span><span class="c1"></span><span class="kn">package</span> <span class="nx">iface</span> <span class="kn">import</span> <span class="s">&#34;your_project/internal/domain&#34;</span> <span class="kd">type</span> <span class="nx">UserRepository</span> <span class="kd">interface</span> <span class="p">{</span> <span class="nx">FindByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">uint</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">domain</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">}</span></code></pre></div> <p>然后,<code>adapter/repository</code> 层去实现它。</p> <div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="c1">// in adapter/repository/user_gorm.go </span><span class="c1"></span><span class="kn">package</span> <span class="nx">repository</span> <span class="c1">// ... </span><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">userRepository</span><span class="p">)</span> <span class="nx">FindByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">uint</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">domain</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// gorm query logic... </span><span class="c1"></span> <span class="c1">// convert gorm model to domain.User </span><span class="c1"></span><span class="p">}</span></code></pre></div> <p>这样做的好处是巨大的:</p> <ol> <li><strong>完美解耦</strong>:<code>usecase</code> 只关心它需要什么,不关心底层如何实现。更换数据库实现对 <code>usecase</code> 毫无影响。</li> <li><strong>易于测试</strong>:在为 <code>usecase</code> 写单元测试时,我们可以轻而易举地 mock 这个 <code>UserRepository</code> 接口,而无需一个真实的数据库连接。</li> <li><strong>符合最小知识原则</strong>:如果让 <code>adapter</code> 来定义接口,它可能会暴露很多 <code>usecase</code> 根本不需要的方法,增加了使用者的心智负担。而由使用者定义,则可以确保接口的精简和必要。</li> </ol> <h3 id="总结">总结</h3> <p>从一个混乱的遗留项目开始,到最终落地一套清晰、可维护的整洁架构,这次重构之旅让我收获颇丰。整洁架构并不仅仅是一套死板的目录结构或分层规则,它更是一种引导我们写出高内聚、低耦合代码的思维方式。</p> <p>它迫使我们去思考: - 什么是业务的核心?(Domain) - 业务流程是怎样的?(UseCase) - 技术细节如何与业务逻辑解耦?(Adapter &amp; Dependency Inversion)</p> <p>通过这次实践,我对依赖倒置、接口隔离等 SOLID 原则有了远比书本上更深刻的理解。当你真正理解了“为什么”要这么做,而不是仅仅停留在“是什么”和“怎么做”的层面时,你会发现,写出整洁、健壮的代码,其实是一件充满乐趣的事情。</p> <p>希望这篇文章能为同样在重构道路上探索的你,提供一些有价值的参考与启发。</p> 打造一个属于自己的AI书签 https://zhu327.github.io/2025/07/07/%E6%89%93%E9%80%A0%E4%B8%80%E4%B8%AA%E5%B1%9E%E4%BA%8E%E8%87%AA%E5%B7%B1%E7%9A%84ai%E4%B9%A6%E7%AD%BE/ Mon, 07 Jul 2025 14:55:52 +0800 https://zhu327.github.io/2025/07/07/%E6%89%93%E9%80%A0%E4%B8%80%E4%B8%AA%E5%B1%9E%E4%BA%8E%E8%87%AA%E5%B7%B1%E7%9A%84ai%E4%B9%A6%E7%AD%BE/ <h3 id="前言">前言</h3> <p>我一直是个重度的书签使用者,多年来都依赖 Pocket 来收藏和沉淀那些在网上看到的、值得一读的文章。然而,最近 Pocket 宣布其通知服务即将关闭,这对我来说像是一个信号,是时候去寻找一个更稳定、更可控的替代品了。</p> <p>我的探索之旅就这样开始了:</p> <ol> <li><strong>Raindrop.io</strong>:社区里很多人推荐,功能也确实强大。但不知道是不是网络原因,我这边访问起来总是感觉有点慢,而且用惯了 Pocket 的我,对它的界面和交互逻辑始终有些不太习惯。</li> <li><strong>自建服务</strong>:作为程序员,第一反应自然是自己动手。我找到了像 <a href="https://github.com/sissbruecker/linkding">linkding</a> 这样的优秀开源项目。我尝试把它部署在 <a href="https://fly.io/">fly.io</a> 的免费实例上,但 linkding 对资源的要求不低,最低配的 256M 内存实例直接就 OOM (Out of Memory) 了,升级配置又意味着成本。</li> <li><strong>从零开始写一个?</strong> 这个念头一闪而过。我甚至构思好了技术选型:参照 linkding 的功能,用我最近在学习的 <strong>Rust</strong> 实现后端,部署在 <strong>Cloudflare Workers</strong> 上,再配上 <strong>Cloudflare D1</strong> 的 SQLite 数据库。这套方案 Serverless、成本极低,堪称完美。但转念一想,这工程量可不小,对于业余项目来说,我实在没有那么多时间投入进去。</li> </ol> <p>就在我快要放弃,准备将就着用某个方案时,我想起了一篇以前收藏过的文章。</p> <p></p> <h3 id="1-灵感乍现-llm-x-github-书签">1. 灵感乍现:LLM x GitHub 书签</h3> <p>那篇文章是 <a href="https://nekonull.me/posts/llm_x_bookmark/">LLM x 书签收藏:摘要 &amp; 全文索引</a>,作者提供了一个极具创意的思路:通过一个浏览器插件 <a href="https://github.com/osmoscraft/osmosmemo">osmos::memo</a>,可以将书签直接保存到指定 GitHub 仓库的 <code>README.md</code> 文件里。</p> <p>这个方案瞬间击中了我!它足够<strong>简单</strong>,没有多余的功能,完美符合我的需求。数据就躺在自己的 GitHub 仓库里,再也不用担心服务关停。虽然原文后续介绍了如何利用 LLM 对书签进行摘要和索引,但我觉得还可以更进一步,改造成一个完全自动化、更符合我使用习惯的流程。</p> <p>说干就干,我立刻创建了一个新的仓库 <a href="https://github.com/zhu327/bookmark">bookmark</a>,准备开始我的改造计划。</p> <h3 id="2-存量数据的漫漫迁移路">2. 存量数据的漫漫迁移路</h3> <p>要打造新家,首先得把老家的家当搬过去。第一步,就是处理从 Pocket 导出的几百条存量书签。</p> <p>我写了一个简单的 Python 脚本,将 Pocket 导出的 HTML 文件解析成 Markdown 格式,然后一股脑儿地塞进了 <code>README.md</code>。但这只是万里长征的第一步。我的目标是让每个书签都有 <strong>AI 生成的摘要和分类</strong>。</p> <p>我想到了最近很火的 Gemini,它的命令行工具 <code>gemini-cli</code> 看起来很适合用来做批处理。于是,我开始了我的踩坑之旅:</p> <ol> <li><strong>初次尝试(失败)</strong>:我用 <strong>Playwright</strong> 写了个脚本,模拟浏览器去访问我的 230 个书签链接,把每个页面的完整 HTML 都抓取下来。结果,所有内容加起来足足有 <strong>165MB</strong>。</li> <li><strong>分割文件 + Gemini CLI(失败)</strong>:我把这 165MB 的内容按每个网页一个文件进行分割,然后尝试用 <code>gemini-cli</code> 遍历所有文件,让它为每个 HTML 生成摘要和分类。结果,Gemini 的上下文窗口有限,而且 API 调用非常非常慢,一个下午过去,才处理了不到 80 个。果断放弃。</li> <li><strong>打包上传 AI Studio(失败)</strong>:我想,单个处理不行,那就打包处理。我用 Python 脚本把网页按每 20 个一组进行打包,但每个包的文件大小仍在 5MB ~ 14MB 之间。这个大小对于直接上传到 Google AI Studio 来说还是太大了,此路不通。</li> <li><strong>精简内容 + 手动处理(成功!)</strong>:看来问题出在原始 HTML 内容太庞杂了。我再次修改脚本,在分割文件的同时,引入 <strong><code>html2text</code></strong> 库,将臃肿的 HTML 转换成纯文本。这下效果立竿见影,每 20 个网页打包成的文件只有 <strong>400KB ~ 600KB</strong>。</li> <li><strong>AI 手工操作</strong>:我将这 13 个(230/20 ≈ 13)处理过的文本文件,<strong>手动一个个上传</strong>到 Google AI Studio。然后,通过精心设计的 Prompt,让 AI 为文件里的每一个链接都生成摘要,并整理成我想要的 Markdown 格式。</li> <li><strong>最终分类</strong>:我把 AI 生成的所有摘要内容汇总到一个 <code>summary.md</code> 文件里。最后,再把这个汇总文件上传给 AI Studio,给它下达最终指令:“请根据这些内容的摘要,将所有链接按技术领域、生活、思考等类别进行分类整理。”</li> </ol> <p>最终,我得到了完美的分类归档文件:<a href="https://github.com/zhu327/bookmark/blob/main/category.md">category.md</a>。虽然过程曲折,但看到整齐划一的成果时,一切都值了。</p> <h3 id="3-新增书签-交给-github-actions-吧">3. 新增书签?交给 GitHub Actions 吧!</h3> <p>处理完存量数据,接下来就要考虑如何自动化地处理每一个新增的书签了。我的思路是,让万能的 <strong>GitHub Actions</strong> 来扮演这个智能管家的角色。</p> <p>整个流程如下:</p> <ol> <li><strong>触发</strong>:每当我通过 <code>osmos::memo</code> 插件向 <code>README.md</code> 添加新链接并推送到 GitHub 时,就会自动触发 GitHub Actions 的 workflow。</li> <li><strong>解析</strong>:在 workflow 中运行的 Python 脚本 (<code>process_bookmarks.py</code>) 会使用 <code>git diff</code> 命令,精准地找出 <code>README.md</code> 中新增加的那一行书签链接。</li> <li><strong>抓取内容</strong>:为了避免自己写爬虫遇到的各种反爬问题,我选择了一个非常棒的工具——<a href="https://jina.ai/reader/">jina reader</a>。通过调用它的 API (<code>https://r.jina.ai/URL</code>),可以直接获取到目标链接页面的干净、规范的 Markdown 文本。</li> <li><strong>生成摘要</strong>:将获取到的 Markdown 文本发送给 LLM API,让它生成一段简洁的摘要。</li> <li><strong>智能分类</strong>:将链接的原始标题和 AI 生成的摘要再次组合,发送给 LLM API,让它判断这个链接应该属于哪个分类。</li> <li><strong>写入归档</strong>:最后,脚本会按照 <code>[标题](链接) - 摘要</code> 的格式,将这条全新的、处理好的书签信息,追加到 <code>category.md</code> 文件对应的分类下。</li> </ol> <p>就这样,一套完全自动化的 AI 书签整理流程就诞生了。我只需要在浏览器上点一下收藏,剩下的抓取、摘要、分类、归档工作,都由我的 AI 管家在云端默默完成。</p> <h3 id="4-拥有你自己的-ai-书签">4. 拥有你自己的 AI 书签</h3> <p>这套方案我已经开源在 GitHub 上:<a href="https://github.com/zhu327/bookmark">https://github.com/zhu327/bookmark</a></p> <p>目前我使用的 LLM API 是由硅基流动提供的免费模型 <strong>deepseek-ai/DeepSeek-R1-0528-Qwen3-8B</strong>,效果相当不错。如果你也对这个方案感兴趣,可以非常简单地拥有自己的 AI 书签:</p> <ol> <li><strong>Fork</strong> 这个仓库。</li> <li>在你的仓库 <code>Settings -&gt; Secrets and variables -&gt; Actions</code> 中,添加两个 <code>Repository secrets</code>: <ul> <li><code>LLM_API_URL</code>: 兼容 OpenAI API 格式的大模型接口地址,例如 <code>https://api.siliconflow.cn/v1/chat/completions</code>。</li> <li><code>OPENAI_API_KEY</code>: 调用该 API 所需的 Key。</li> </ul></li> </ol> <p>当然,这个方案也并非完美。比如对于有严格反爬策略的网站(点名微信公众号),<code>jina reader</code> 也无能为力,无法直接获取到文章内容。</p> <h3 id="总结">总结</h3> <p>在后 Pocket 时代,借助 LLM 的强大能力,我的个人书签系统完成了一次有趣的进化。它不再是一个依赖于第三方在线服务的传统书签,而是变成了一个由我自己掌控、活在 GitHub 上的智能知识库。所有数据都以 Markdown 格式清晰地存储,检索也变得前所未有的简单——打开 <code>category.md</code>,按下 <code>CTRL + F</code> 即可。</p> <p>这个小项目让我再次感受到,将 AI 作为一种工具融入到个人工作流中,能够迸发出巨大的能量。它不仅解决了我的实际问题,也让整个过程充满了探索和创造的乐趣。</p> 我理解的AI Agent与MCP https://zhu327.github.io/2025/04/02/%E6%88%91%E7%90%86%E8%A7%A3%E7%9A%84ai-agent%E4%B8%8Emcp/ Wed, 02 Apr 2025 10:55:52 +0800 https://zhu327.github.io/2025/04/02/%E6%88%91%E7%90%86%E8%A7%A3%E7%9A%84ai-agent%E4%B8%8Emcp/ <h3 id="前言">前言</h3> <p>最近AI圈子里Manus这个项目可以说是相当火了,连带着MCP(Model Context Protocol)这个概念也开始频繁出现在各种技术讨论区。作为一个程序员,自然也对这些新东西充满了好奇,于是花时间捣鼓了一下AI Agent和MCP相关的知识。这篇文章就是我学习和思考后,对这两个概念的一些个人理解和梳理。</p> <h3 id="1-从workflow到ai-agent-大脑的进化">1. 从Workflow到AI Agent:大脑的进化</h3> <p>回顾我们以前做自动化任务,比如搞个自动抢票、自动下单之类的,思路基本都是围绕着一个固定的<strong>workflow</strong>。我们会预先设定好一套流程逻辑:收到指令 -&gt; 判断条件 -&gt; 走对应分支 -&gt; 调用特定工具(API、脚本等)。整个过程就像一个流程图,每个节点和路径都是我们程序员预先设计好的。在这个模式下,<strong>我们程序员就是这个workflow的“大脑”</strong>,我们把执行计划通过代码逻辑,“硬编码”到系统里。比如预定机票这个场景,我们会写代码一步步调用查询接口、选择航班接口、填写乘机人信息接口、支付接口等等。</p> <p></p> <h3 id="2-llm登场-有了-大脑-但还缺-手脚">2. LLM登场:有了“大脑”,但还缺“手脚”</h3> <p>随着大语言模型(LLM)技术的突飞猛进,特别是像DeepSeek R1这样强大的推理模型出现,它们的推理和规划能力越来越接近人类大脑。这就让我们自然而然地想到:能不能让LLM来充当这个自动化流程的“大脑”,代替我们来制定执行计划呢?</p> <p>想法很美好,但LLM本质上还是一个<strong>文本补全</strong>的模型。它能“思考”并告诉你计划是什么,但它自己并不能直接撸起袖子去调用那些具体的工具(API、函数等)来完成任务。为了解决这个问题,OpenAI在其API中引入了<code>tools</code>(或称为 Function Calling)的概念。</p> <p>这个流程大致是这样的:</p> <ol> <li>我们的应用程序(可以称为Agent)调用LLM API时,在请求的上下文中详细描述可用的工具:每个工具是干嘛的、输入需要什么参数、输出是什么格式。</li> <li>Agent把用户的自然语言指令(Prompt)发给LLM。</li> <li>LLM根据用户的需求,结合它所知道的工具信息,判断是否需要以及需要调用哪个(或哪些)工具。</li> <li>如果需要调用工具,LLM不会直接执行,而是<strong>返回一个包含调用指令的响应</strong>,里面说明了要调用哪个工具以及需要传递的参数(通常是JSON格式)。</li> <li>Agent收到这个响应后,解析出调用指令,<strong>在Agent自己的环境里执行</strong>相应的工具(比如调用一个本地函数或一个外部API)。</li> <li>Agent拿到工具的执行结果后,把这个结果再次作为上下文信息,传回给LLM。</li> <li>LLM根据工具的执行结果,决定下一步是继续调用其他工具,还是生成最终的回复给用户。</li> </ol> <p>这个基于<code>tools</code>的交互模式,让LLM有了指挥“手脚”(工具)的能力。但这里也暴露了一个比较明显的<strong>弊端</strong>:完成一个任务可能需要<strong>多次与LLM进行交互</strong>(LLM决策 -&gt; Agent执行 -&gt; Agent反馈结果 -&gt; LLM再决策&hellip;),并且为了让LLM理解上下文,每次交互都需要传递越来越长的历史信息,这既<strong>增加了延迟</strong>,也<strong>增加了API调用成本和Token消耗</strong>。</p> <h3 id="3-codeact-让llm直接写代码执行">3. CodeAct:让LLM直接写代码执行</h3> <p>我们知道,当前LLM最强的能力之一就是<strong>代码生成</strong>。于是,就有人想出了另一种更直接的思路:既然LLM擅长写代码,而我们提供的工具本身很多就是函数或者可以通过代码调用的API,那何不让LLM直接生成一小段可执行的代码(比如Python),然后在我们的Agent里内置一个安全的<strong>代码执行沙箱</strong>来运行这段代码呢?</p> <p>在这段生成的代码里,LLM可以直接包含对我们提供的工具(函数)的调用,甚至可以编写一些简单的流程控制逻辑(if/else、循环等)。这样一来,多个步骤和工具调用可能通过一次LLM生成、一次代码执行就完成了,大大<strong>减少了与LLM的交互次数</strong>。这种方法就被称为<strong>CodeAct</strong>(代码作为行动)。</p> <h3 id="4-到底什么是ai-agent">4. 到底什么是AI Agent?</h3> <p>聊了这么多,我们再来尝试给AI Agent下一个定义。我认为,<strong>AI Agent = LLM(大脑) + Tools(手脚) + Agent Runtime(执行器) + 决策循环</strong>。</p> <p>具体来说,AI Agent通过调用LLM API,并结合一系列可用的工具(Tools),来理解并执行用户的自然语言任务。它通常采用一种<strong>决策-执行-观察</strong>的循环模式(ReAct):</p> <ol> <li><strong>决策(Reason)</strong>:由LLM根据当前任务目标和上下文信息,决定下一步是调用工具还是直接回复。</li> <li><strong>执行(Act)</strong>:由Agent Runtime根据LLM的决策,执行相应的工具调用或代码片段。</li> <li><strong>观察(Observe)</strong>:Agent Runtime将执行结果反馈给LLM,作为新的上下文信息,供LLM进行下一轮决策。</li> </ol> <p>这个循环一直持续,直到任务完成。</p> <p>但这里有个关键问题:LLM本身具有一定的<strong>不确定性</strong>。对于相同的输入(Prompt),它每次返回的结果可能都不完全一样。这就给AI Agent的执行带来了挑战,有时候可能会偏离预期或者卡在某个环节。因此,在开发AI Agent时,接入<strong>Tracing(追踪)</strong>系统变得非常重要。我们需要能够实时跟踪Agent与LLM的每一次交互、上下文内容、工具调用情况以及执行结果,这样才能在出现问题时快速定位和调试,进而优化我们的Prompt或者Agent逻辑。</p> <h3 id="5-多agent协作-专业的事交给专业的agent">5. 多Agent协作:专业的事交给专业的Agent</h3> <p>随着Manus的流行,<strong>多Agent编排(Multi-Agent Orchestration)</strong>的理念也开始受到关注。核心思路是<strong>化整为零,分工协作</strong>。</p> <p>我们不再试图构建一个“万能”的超级Agent,而是将复杂的任务拆解,构建多个<strong>功能单一、职责明确</strong>的<strong>专家Agent</strong>。比如,我们可以把<code>web_search</code>和<code>web_fetch</code>这两个工具组合起来,封装成一个专门负责网页抓取的<code>web_scraper_agent</code>。这种单一功能的Agent,它的大脑(LLM)可能不需要最顶级的推理能力,我们可以选用一个<strong>性能足够且成本更低</strong>的模型(比如一些中小型模型或者特定领域的微调模型)。</p> <p>当我们有了多个这样的专家Agent后,再引入一个<strong>Planner Agent(规划器Agent)</strong>。这个Planner Agent通常需要一个<strong>推理能力较强</strong>的LLM作为大脑,它的职责是:</p> <ol> <li>接收用户的原始任务。</li> <li>将任务拆解成若干子步骤。</li> <li>根据每个子步骤的需求,选择并调用合适的专家Agent。</li> <li>收集专家Agent的执行结果。</li> <li>根据执行结果,动态调整后续的计划(可能需要重新规划或调用其他Agent)。</li> <li>循环这个过程,直到最终任务完成,然后将结果汇总返回给用户。</li> </ol> <p>这种多Agent架构带来的好处显而易见:</p> <ul> <li><strong>成本效益</strong>:在大量执行具体任务的专家Agent上可以使用更便宜的LLM。</li> <li><strong>维护性</strong>:每个Agent功能单一,更容易开发、测试和维护。</li> <li><strong>扩展性</strong>:可以方便地增加新的专家Agent来扩展整个系统的能力。</li> <li><strong>可能更优化的上下文管理</strong>:Planner Agent可能只需要关注子任务的目标和专家Agent返回的关键结果,而不需要承载所有底层工具调用的详细上下文。</li> </ul> <h3 id="6-prompt工程-agent的灵魂">6. Prompt工程:Agent的灵魂</h3> <p>无论是使用ReAct还是CodeAct,无论是单Agent架构还是多Agent编排,有一个环节始终是绕不开的,那就是<strong>Prompt Engineering(提示工程)</strong>。</p> <p>我们需要为每一个与LLM交互的环节(无论是Planner Agent制定计划,还是专家Agent执行任务,或是Function Calling/CodeAct的触发)<strong>精心设计Prompt</strong>。好的Prompt需要能够引导LLM按照我们的预期来思考和输出:</p> <ul> <li>让Planner Agent生成<strong>结构化、可分步执行</strong>的计划。</li> <li>让LLM在需要时<strong>准确地选择并调用</strong>我们提供的工具。</li> <li>让LLM返回<strong>格式规范、易于解析</strong>的文本(比如JSON),方便Agent Runtime处理。</li> </ul> <p>可以说,Prompt就是我们给AI Agent注入灵魂的方式。</p> <h3 id="7-mcp-agent间沟通的标准语">7. MCP:Agent间沟通的标准语</h3> <p>随着多Agent架构的兴起,Agent之间如何发现彼此、如何调用彼此的能力就成了一个问题。这时,<strong>MCP(Model Context Protocol)</strong>这个概念应运而生。</p> <p>你可以把MCP理解为一套<strong>为Agent(或者说LLM的工具)设计的标准化接口规范</strong>。</p> <p>在没有MCP之前,我们实现的工具可能五花八门:一段本地Python代码、一个内部HTTP API调用、一个第三方云服务接口等等。每次要给Agent添加新工具,都需要手动编写关于这个工具的描述(功能、参数、格式),然后注入到Agent的上下文中。</p> <p>有了MCP之后,情况就可能变得更规范:</p> <ul> <li>工具提供方可以将其能力封装成一个遵循MCP协议的<strong>MCP Server</strong>。</li> <li>Agent可以通过标准的MCP方法(比如<code>list_tools</code>)来<strong>动态发现</strong>某个MCP Server提供了哪些工具及其描述。</li> <li>Agent可以通过标准的MCP <strong>RPC Call</strong>来执行这些工具。</li> </ul> <p>MCP不仅仅规范了工具的调用,它通常也试图规范<strong>Prompt模板、资源管理</strong>等Agent开发中常见的元素。如果MCP能够被广泛采用,理论上可以:</p> <ul> <li><strong>降低Agent的开发门槛</strong>:开发者可以直接复用社区提供的、遵循MCP标准的工具服务,而不需要自己重复实现和描述工具。</li> <li><strong>提高Agent的互操作性</strong>:不同开发者、不同团队开发的Agent或许能更容易地进行协作。</li> </ul> <p>当然,MCP目前似乎还在发展初期,但它指明了一个让Agent生态更加规范化、标准化的方向。</p> <h3 id="8-总结-门槛不高-精通不易">8. 总结:门槛不高,精通不易</h3> <p>以上就是我这段时间学习下来,对AI Agent和MCP的一些粗浅理解。在阅读了几个开源的Manus实现(或者类似的多Agent框架)之后,我的一个直观感受是:</p> <p>对于掌握Python的程序员来说,上手开发一个基础的AI Agent,或者编写一个简单的Tool/MCP Server,<strong>技术门槛似乎并不算特别高</strong>。很多框架(如LangChain、LlamaIndex等)已经帮我们处理了与LLM交互、管理上下文、执行工具等繁琐的底层工作。</p> <p>然而,<strong>真正的挑战在于</strong>:如何在一个<strong>特定的业务场景</strong>下,设计和实现一个<strong>稳定、可靠、高效</strong>的AI Agent。这需要:</p> <ul> <li><strong>深刻理解业务需求</strong>,并将其转化为适合Agent执行的任务流程。</li> <li><strong>精心的Prompt设计与调优</strong>,不断尝试,引导LLM给出期望的输出。</li> <li><strong>有效的Tracing和Debugging</strong>,快速定位Agent执行过程中的问题并进行修复。</li> <li><strong>合理的架构选择</strong>(单Agent vs 多Agent,ReAct vs CodeAct)和<strong>成本控制</strong>。</li> </ul> <p>就像当年我们从写简单脚本到构建复杂系统一样,AI Agent的世界也是易学难精。工具和框架降低了入门门槛,但要做出真正好用的Agent,还需要大量的实践、思考和打磨。</p> <h3 id="9-参考">9. 参考</h3> <ul> <li><a href="https://arthurchiao.art/blog/ai-agent-white-paper-zh/">Google AI Agent(智能体)技术白皮书</a></li> <li><a href="https://arthurchiao.art/blog/build-effective-ai-agent-zh/">Anthropic AI Workflow &amp; AI Agent:架构、模式与工程建议</a></li> <li><a href="https://huggingface.co/learn/agents-course/zh-CN/unit0/introduction">Hugging Face AI Agents 课程</a></li> <li><a href="https://github.com/liaokongVFX/MCP-Chinese-Getting-Started-Guide">Model Context Protocol(MCP) 编程极速入门</a></li> <li><a href="https://github.com/liaokongVFX/LangChain-Chinese-Getting-Started-Guide">LangChain 中文入门教程</a></li> <li><a href="https://github.com/mannaandpoem/OpenManus">OpenManus</a></li> <li><a href="https://github.com/Darwin-lfl/langmanus">LangManus</a></li> </ul> 云原生开发入门 https://zhu327.github.io/2025/03/28/%E4%BA%91%E5%8E%9F%E7%94%9F%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8/ Fri, 28 Mar 2025 10:55:52 +0800 https://zhu327.github.io/2025/03/28/%E4%BA%91%E5%8E%9F%E7%94%9F%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8/ <p>作为一名在云原生领域摸爬滚打了一段时间的开发者,我时常会遇到一些刚接触这个领域的朋友,他们面对 Kubernetes (K8s)、容器这些概念时,常常感到有些迷茫,不知道从何学起。这篇文章,我想结合自己的一些学习体会,为想要入门云原生开发的同学提供一个相对清晰的指引。</p> <p>我自己并非科班出身,从最初接触软件测试写 Lua 脚本,到后来学习 Python 做后端,再到进入腾讯接触 Golang 和 K8s 相关开发,一路走来也踩了不少坑。云原生技术栈确实庞大,但核心的理念是利用容器、微服务、声明式 API 等方式,去构建更可靠、更具弹性的分布式系统。而 K8s 作为这个生态的核心编排引擎,可以说是我们必须掌握的关键技能。</p> <p>这篇文章,我会从基础概念聊起,分享几本我学习过程中觉得非常有帮助的书籍,一个不错的社区文章集合,以及两个可以动手实践的代码项目。最后,还会结合实际工作经验,聊聊 K8s 运维实践中一些重要的事情。希望这些能帮助大家在学习云原生的路上少走一些弯路。</p> <hr /> <h2 id="一-云原生与-kubernetes-从基础概念开始">一、云原生与 Kubernetes:从基础概念开始</h2> <h3 id="云原生到底是什么">云原生到底是什么?</h3> <p>我理解的云原生(Cloud Native),它不是指某一个具体的技术,更像是一种面向云环境的应用设计、开发和部署的最佳实践集合。它鼓励我们使用像容器、微服务、不可变基础设施、声明式 API 这些技术和方法,目标是让我们的应用能更好地利用云计算的弹性、分布式、自动化等优势。根据 CNCF(云原生计算基金会)的定义,这些是关键要素。简单说,就是让应用为云而生,能在云上运行得更好。</p> <h3 id="为什么-kubernetes-这么重要">为什么 Kubernetes 这么重要?</h3> <p>容器技术(比如 Docker)解决了环境一致性的问题,但当你有成百上千个容器需要管理时,手动操作就变得不现实了。Kubernetes 就是来解决这个问题的。它是一个开源的容器编排平台,最初由 Google 开发,现在由 CNCF 维护。它可以自动化地完成应用的部署、扩缩容、服务发现、负载均衡、故障恢复等一系列复杂工作。在我看来,掌握 K8s 是进入现代后端开发、特别是平台工程领域的一张重要门票。</p> <h3 id="我的学习路径建议">我的学习路径建议:</h3> <p>回顾我自己的学习过程,我建议可以按以下步骤来:</p> <ol> <li><strong>先搞懂容器</strong>:理解 Docker 的基本概念,比如镜像(Image)、容器(Container)、Dockerfile,能自己动手构建、运行一个简单的容器化应用。</li> <li><strong>再深入 K8s 核心</strong>:学习 K8s 的核心组件(比如 Pod、Service、Deployment、StatefulSet、Namespace 等)都是做什么的,它们之间是如何协作的。了解控制平面(Master)和数据平面(Node)的基本工作原理。</li> <li><strong>动手实践是关键</strong>:理论学习后,一定要动手实践。尝试编写 YAML 文件部署应用,使用 <code>kubectl</code> 与集群交互。然后可以去探索存储(Volumes, PV, PVC)、网络(Ingress, CNI)、配置(ConfigMap, Secret)等更深入的主题。</li> <li><strong>理解运维视角</strong>:开发的应用最终要跑在生产环境。了解 K8s 集群的监控、日志、告警、排错等运维实践,对于写出更健壮、更易于维护的应用非常有帮助。</li> </ol> <p>接下来,分享一些我在学习过程中用过且觉得不错的资源。</p> <p></p> <hr /> <h2 id="二-推荐书籍-理论与实践结合">二、推荐书籍:理论与实践结合</h2> <p>市面上关于 K8s 的书不少,但下面这两本是我个人觉得非常经典,值得花时间去读的。</p> <h3 id="1-kubernetes-in-action">1. 《Kubernetes in Action》</h3> <ul> <li><strong>豆瓣评分参考</strong>:8.8 分</li> <li><strong>我的推荐理由</strong>: 这本书非常侧重实践。作者 Marko Lukša 通过大量可操作的例子,一步步带你上手 K8s。从最基础的 Pod、Service 到 Deployment 实现滚动更新,都讲得很清楚。对于刚入门 K8s,希望快速看到效果、建立信心的同学来说,这本书是极好的选择。我当初就是跟着这本书敲代码,对 K8s 的基本操作有了直观的认识。</li> <li><strong>可以重点关注的章节</strong>: <ul> <li><strong>第 2 章</strong>:用一个 Node.js 例子带你跑通第一个 K8s 应用。</li> <li><strong>第 5 章</strong>:讲解 Service,理解应用如何被访问。</li> <li><strong>第 9 章</strong>:讲解 Deployment,掌握应用发布的核心方式。</li> </ul></li> <li><strong>适合谁</strong>:K8s 新手,特别是喜欢通过动手实践来学习的开发者。</li> </ul> <h3 id="2-深入剖析-kubernetes">2. 《深入剖析 Kubernetes》</h3> <ul> <li><strong>豆瓣评分参考</strong>:9.3 分</li> <li><strong>我的推荐理由</strong>: 张磊大佬写的这本书,更侧重于 K8s 的设计理念和底层实现原理。如果你不满足于仅仅会用 K8s,还想知道它内部是怎么工作的(比如调度器如何决策?网络 CNI 如何实现?),那这本书绝对值得一读。对我自己而言,阅读这本书加深了我对 K8s 架构的理解,对于后来做一些 K8s 相关的开发工作非常有帮助。它不仅仅是讲技术,还融入了作者在一线的实践思考。</li> <li><strong>可以重点关注的章节</strong>: <ul> <li><strong>第 3 章</strong>:K8s 整体架构和设计哲学。</li> <li><strong>第 5 章</strong>:控制器(Controller)和编排原理,这是 K8s 自动化的核心。</li> <li><strong>第 7 章</strong>:网络原理,CNI、Service 等机制讲得很透彻。</li> </ul></li> <li><strong>适合谁</strong>:对 K8s 有一定基础,想深入理解其工作原理和设计思想的开发者。</li> </ul> <p><strong>怎么选?</strong> 我的建议是,如果你是新手,可以先看《Kubernetes in Action》动手实践,建立基本概念;然后再读《深入剖析 Kubernetes》加深理解。如果你已经有基础,可以直接看《深入剖析 Kubernetes》。</p> <hr /> <h2 id="三-社区文章合集-k8s-club">三、社区文章合集:K8s Club</h2> <p>除了书籍,社区里也有很多高质量的文章。我个人比较关注 <a href="https://github.com/k8s-club/k8s-club">K8s Club</a> 这个 GitHub 仓库。</p> <p>它汇集了不少国内云原生开发者撰写的技术文章,内容覆盖从基础到进阶,也有源码分析和实践经验分享,质量普遍不错。</p> <h3 id="值得关注的内容">值得关注的内容:</h3> <ul> <li><strong>概念梳理与入门</strong>:比如用问答形式总结 K8s 核心概念的文章。</li> <li><strong>知识图谱</strong>:帮助你了解 K8s 的整体知识体系。</li> <li><strong>核心组件原理分析</strong>:比如 Informer 机制、调度器原理等,对深入理解 K8s 或做二次开发很有帮助。</li> <li><strong>实践操作指南</strong>:比如手动搭建集群、Pod 生命周期管理等。</li> </ul> <p><strong>如何利用?</strong> 遇到具体问题时可以去搜索相关的文章;也可以系统性地阅读某个系列;最重要的是,看完之后尽量动手在自己的环境(比如 Minikube)中验证一下。</p> <hr /> <h2 id="四-代码示例项目-动手实践出真知">四、代码示例项目:动手实践出真知</h2> <p>理论学习最终还是要落到实践上。就像我学习 Rust 会自己动手写项目一样,学习 K8s 也需要通过代码来加深理解。下面是我找到的两个 GitHub 上的项目集合,提供了不错的练手机会:</p> <h3 id="1-kubernetes-learning-playground">1. Kubernetes Learning Playground</h3> <ul> <li><strong>地址</strong>:<a href="https://github.com/Kubernetes-Learning-Playground">https://github.com/Kubernetes-Learning-Playground</a></li> <li><strong>简介</strong>:这个组织下有不少关于 K8s 扩展开发的项目示例,比如如何与 K8s API 交互(client-go)、简单的 CSI/CNI 插件实现、自定义调度器等。</li> <li><strong>可以看看的项目</strong>: <ul> <li><code>multi-cluster-informer</code>:学习如何监控多个 K8s 集群。</li> <li><code>csi-interface-study</code>:一个简单的 CSI 存储插件示例。</li> </ul></li> <li><strong>适合</strong>:对 K8s 内部机制、API 编程、插件开发感兴趣的同学。</li> </ul> <h3 id="2-operator-learning-playground">2. Operator Learning Playground</h3> <ul> <li><strong>地址</strong>:<a href="https://github.com/Operator-Learning-Playground">https://github.com/Operator-Learning-Playground</a></li> <li><strong>简介</strong>:专注于 K8s Operator 开发模式的练习项目。Operator 是用来管理有状态应用或自动化复杂运维任务的重要模式。</li> <li><strong>可以看看的项目</strong>: <ul> <li><code>proxy-operator</code>:一个简单的 Operator 例子,感受下基本结构。</li> <li><code>podReStarter-operator</code>:实现特定条件下自动重启 Pod 的控制器。</li> </ul></li> <li><strong>适合</strong>:想学习如何使用 CRD (自定义资源) 和 Controller 来扩展 K8s 功能的同学。</li> </ul> <p><strong>实践建议</strong>: * 在本地用 Minikube 或 Kind 搭个 K8s 环境,方便实验。 * 从简单的项目开始,先跑起来,再尝试理解代码,然后可以试着修改或扩展它。</p> <hr /> <h2 id="五-学习路线与个人心得">五、学习路线与个人心得</h2> <p>结合上面的资源,我梳理了一个学习 K8s 的大致路线:</p> <ol> <li><strong>基础入门</strong>:通过《Kubernetes in Action》和动手实验,掌握 Docker 和 K8s 的基本概念与操作。能部署简单应用,会用 <code>kubectl</code>。</li> <li><strong>深入原理</strong>:阅读《深入剖析 Kubernetes》,理解 K8s 的核心架构和工作机制。结合 K8s Club 的文章,针对性学习。</li> <li><strong>实践进阶</strong>:研究上面提到的代码示例项目,尝试理解并动手修改。开始关注 K8s 生态中的其他工具,如 Helm、Prometheus 等。</li> <li><strong>持续学习</strong>:云原生技术发展很快,保持学习状态很重要。多看官方文档,关注社区动态。</li> </ol> <p><strong>我自己的一些体会:</strong></p> <ul> <li><strong>动手非常重要</strong>:看再多文档不如自己动手敲一遍。遇到问题、解决问题的过程是最好的学习。</li> <li><strong>非科班不用怕</strong>:像我一样非科班出身的同学,可能基础会薄弱一些,那就多花点时间去补基础(比如《深入理解计算机系统》这样的书)。关键是持续学习和实践。</li> <li><strong>利用好 GitHub</strong>:无论是学习别人的项目,还是像我一样把自己写的东西放上去,GitHub 都是一个很好的平台。它不仅能记录你的成长,有时还能带来意想不到的机会。</li> </ul> <hr /> <h2 id="六-k8s-运维实践-开发者也需关注">六、K8s 运维实践:开发者也需关注</h2> <p>开发完应用部署到 K8s 只是第一步,如何保障它在生产环境稳定运行,是同样重要的事情。虽然可能有专门的运维团队,但作为开发者,了解一些基本的运维实践,能帮助我们写出更符合生产要求的应用,也能在排查问题时更有思路。</p> <p>这部分我参考了腾讯云 K8s 运维开发工程师整理的《<a href="https://imroc.cc/kubernetes/best-practices">Kubernetes 实践指南</a>》(这份指南写得挺实用的),结合自己的一些经验,提炼几点:</p> <ol> <li><strong>熟练使用 <code>kubectl</code></strong>:这是与 K8s 交互的基础。<code>get</code>, <code>describe</code>, <code>logs</code>, <code>exec</code> 等命令要常用常练。学会看 <code>describe</code> 里的 Events 信息,对排错很有帮助。</li> <li><strong>镜像管理</strong>:注意使用小的基础镜像、多阶段构建来优化镜像体积。生产环境用私有镜像仓库,配置好拉取密钥。</li> <li><strong>监控告警是标配</strong>:Prometheus + Grafana 是社区主流方案,需要了解如何暴露应用的 metrics,如何配置基本的告警规则。日志收集(如 EFK/ELK)也要考虑。</li> <li><strong>资源管理(Requests &amp; Limits)</strong>:一定要给 Pod 设置合理的 CPU 和内存 <code>requests</code>(用于调度)和 <code>limits</code>(用于限制),避免资源抢占导致应用不稳定或影响其他应用。</li> <li><strong>健康检查(Probes)</strong>:配置好 Liveness Probe(存活探针,判断容器是否需要重启)和 Readiness Probe(就绪探针,判断容器是否准备好接收流量),让 K8s 能准确管理应用状态。</li> <li><strong>理解 Namespace 和 RBAC</strong>:Namespace 用于资源隔离,RBAC 用于权限控制,都是保障集群安全和管理有序性的重要机制。</li> </ol> <p><strong>运维学习建议</strong>:同样需要动手,可以在本地环境模拟一些场景,比如手动删除 Pod 看 K8s 如何恢复,调整资源限制看效果等。</p> <hr /> <h2 id="七-结语">七、结语</h2> <p>回头看,云原生开发这条路确实内容不少,但走下来收获也很大。从 K8s 的基础操作到深入理解其原理,再到关注运维实践,是一个逐步深入的过程。</p> <p>希望我分享的这些学习资源和路径建议,能对正在入门或打算入门云原生开发的你有所帮助。我自己也是在不断学习中,云原生技术日新月异,保持好奇心和动手实践的热情非常重要。</p> <p>如果你在学习过程中有任何问题或心得,也欢迎一起交流。</p> 从《软件设计的哲学》谈代码的复杂性与应对 https://zhu327.github.io/2025/01/10/%E4%BB%8E%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%93%B2%E5%AD%A6%E8%B0%88%E4%BB%A3%E7%A0%81%E7%9A%84%E5%A4%8D%E6%9D%82%E6%80%A7%E4%B8%8E%E5%BA%94%E5%AF%B9/ Fri, 10 Jan 2025 10:55:52 +0800 https://zhu327.github.io/2025/01/10/%E4%BB%8E%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%93%B2%E5%AD%A6%E8%B0%88%E4%BB%A3%E7%A0%81%E7%9A%84%E5%A4%8D%E6%9D%82%E6%80%A7%E4%B8%8E%E5%BA%94%E5%AF%B9/ <p>几个月前读完了《软件设计的哲学》这本书,在结合自己多年的工作体会,对于代码的可读性,可维护性有了系统性的体会,然后基于我记录的读书笔记然后经过AI的润色,形成了这篇笔记。希望对你也有所帮助。</p> <p>读完《软件设计的哲学》,感觉像经历了一次代码的“深度体检”。这本书没有讲解具体的编码技巧,而是从更高的维度帮助我们重新审视代码复杂性的问题,并提出了许多实用的建议。今天,我想结合自己的理解,分享这本书带来的启发。</p> <h3 id="什么是代码的复杂性">什么是代码的复杂性?</h3> <p>简单来说,复杂性就是那些让系统难以理解和修改的因素。它不仅仅指代码的行数或逻辑深度,还包括:</p> <ul> <li><strong>难以理解</strong>:即便是自己写的代码,过一段时间可能也看不懂,更别提其他人。</li> <li><strong>修改成本高</strong>:一个小改动需要改动多个地方,甚至引发新的问题。</li> <li><strong>模糊的改动范围</strong>:不知道该修改哪些模块才能实现目标,或者不确定修改后是否会引发其他问题。</li> <li><strong>难以修复的错误</strong>:修复一个 bug 可能需要大量时间,还可能引入新的问题。</li> </ul> <p></p> <h3 id="复杂性的表现形式">复杂性的表现形式</h3> <p>复杂性并非抽象概念,它常以以下形式体现:</p> <ul> <li><strong>变更放大</strong>:一个简单需求改动需要修改多个模块甚至代码库。</li> <li><strong>认知负荷</strong>:完成一个任务需要了解大量背景知识或复杂细节。</li> <li><strong>未知的未知</strong>:修改代码时,不确定是否会引入新的问题。</li> </ul> <h3 id="复杂性的来源">复杂性的来源</h3> <p>复杂性通常来源于以下几点:</p> <ul> <li><strong>模块之间的依赖</strong>:一个模块的改动可能需要同步修改依赖它的其他模块。</li> <li><strong>晦涩难懂的代码</strong>:不清晰的变量名、函数名以及缺乏注释的逻辑。</li> <li><strong>文档不足</strong>:缺乏解释代码意图和约束的文档。</li> </ul> <h3 id="面对复杂性的两大策略">面对复杂性的两大策略</h3> <p>书中提到了解决代码复杂性的两种核心策略:</p> <ol> <li><strong>让代码更简单、更直观</strong> <ul> <li>通过清晰的命名、简洁的逻辑、规范的实现,让代码易于理解。</li> </ul></li> <li><strong>封装复杂性</strong> <ul> <li>将复杂逻辑封装起来,通过简洁的接口暴露给外部,用户无需关心内部细节。</li> </ul></li> </ol> <h3 id="如何应对复杂性">如何应对复杂性?</h3> <p>以下是书中提到的一些有效方法:</p> <ol> <li><p><strong>模块化设计</strong></p> <ul> <li>将系统分解为独立模块,减少模块间的依赖。每个模块应只负责一个明确任务,提供清晰接口。</li> </ul></li> <li><p><strong>抽象</strong></p> <ul> <li>忽略不重要的细节,只暴露必要的信息。好的抽象能降低认知负荷并提高代码复用性。</li> </ul></li> <li><p><strong>信息隐藏</strong></p> <ul> <li>隐藏模块的内部实现细节,只暴露接口。这样能减少模块之间的耦合,降低修改风险。</li> </ul></li> <li><p><strong>设计通用接口</strong></p> <ul> <li>通用接口能隐藏细节并提高代码复用性。</li> </ul></li> <li><p><strong>专注于任务的知识</strong></p> <ul> <li>设计模块时,应关注任务所需的知识,而非任务发生的顺序。</li> </ul></li> <li><p><strong>注释代码</strong></p> <ul> <li>注释应解释代码的“为什么”,而非“是什么”。例如,描述变量单位、边界条件等。</li> </ul></li> <li><p><strong>清晰命名</strong></p> <ul> <li>使用准确且一致的命名,避免歧义。</li> </ul></li> <li><p><strong>先写注释,再写代码</strong></p> <ul> <li>先用注释明确表达代码意图,然后再实现。</li> </ul></li> <li><p><strong>设计两次</strong></p> <ul> <li>初步完成代码后,重新审视设计,思考是否有更好的接口和实现方式。</li> </ul></li> <li><p><strong>统一规范</strong></p> <ul> <li>制定团队代码规范,保持一致性,提高可读性。</li> </ul></li> <li><p><strong>显式代码优于隐式代码</strong></p> <ul> <li>代码应尽可能清晰易懂,避免晦涩技巧。</li> </ul></li> </ol> <h3 id="危险信号-什么时候该警惕复杂性">危险信号:什么时候该警惕复杂性?</h3> <p>以下是书中提到的一些“危险信号”,遇到这些情况时,需要考虑重构:</p> <ul> <li><strong>浅模块</strong>:接口与实现的复杂性相差无几。</li> <li><strong>信息泄漏</strong>:设计决策导致多个模块之间耦合。</li> <li><strong>时间分解</strong>:代码结构依赖操作执行顺序,而非基于信息隐藏。</li> <li><strong>过度暴露</strong>:API 暴露了不常用的功能。</li> <li><strong>重复代码</strong>:相同代码反复出现。</li> <li><strong>联合方法</strong>:两个方法间依赖性强,难以单独理解。</li> <li><strong>注释重复代码</strong>:注释仅重复代码内容。</li> <li><strong>含糊不清的名称</strong>:变量或方法名称传递不了有用信息。</li> <li><strong>难以描述</strong>:需要冗长文档解释变量或方法意图。</li> <li><strong>非显而易见的代码</strong>:代码行为或意图不直观。</li> </ul> <h3 id="软件设计的核心原则">软件设计的核心原则</h3> <p>书中总结了以下关键原则:</p> <ol> <li><strong>复杂性是逐步增加的</strong>:从小事做起,持续改进。</li> <li><strong>代码跑起来并不够</strong>:还需关注可读性和可维护性。</li> <li><strong>持续投入改善系统设计</strong>:不要忽视代码的长期维护成本。</li> <li><strong>模块应该足够深</strong>:接口要比实现简单得多。</li> <li><strong>简化接口</strong>:接口设计应尽可能简化最常见用法。</li> <li><strong>通用与专用分离</strong>:通用代码与专用代码应明确区分。</li> <li><strong>层次分明</strong>:不同层次应有不同抽象。</li> <li><strong>降低复杂性</strong>:始终关注减少认知负荷和模块耦合。</li> <li><strong>先设计,再实现</strong>:避免仓促决策,多次设计迭代。</li> <li><strong>注释不明显内容</strong>:好的注释可显著降低认知负荷。</li> <li><strong>代码是写给人看的</strong>:清晰性优先于实现技巧。</li> </ol> <h3 id="结语">结语</h3> <p>《软件设计的哲学》让我意识到,软件开发不仅是实现功能,更是一门需要深思熟虑的艺术。它提醒我们,代码的核心目标是<strong>易于理解和维护</strong>。毕竟,<strong>代码是写给人看的,其次才是给机器看的</strong>。希望这些心得对你有所启发,让我们一起写出更清晰、更优雅的代码!</p> 如何快速上手项目 https://zhu327.github.io/2025/01/08/%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E9%A1%B9%E7%9B%AE/ Wed, 08 Jan 2025 10:55:52 +0800 https://zhu327.github.io/2025/01/08/%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E9%A1%B9%E7%9B%AE/ <p>职业生涯中接手过很多项目了,各种项目都接触过,一个对新人友好的项目一般具备如下特征:</p> <ol> <li>完备的安装/功能文档</li> <li>完备的技术方案,架构设计文档</li> <li>合理的分层,清晰的代码目录结构</li> <li>可读,可维护性高的代码</li> </ol> <p>但是往往我们会忽略一个一个简单的指引性的文档:<strong>Quick Start</strong>,这个文档不需要很详细,只需要说明,如何快速上手的过程,具体的步骤链接到对应的文档即可。一般我们会在Quick Start上写明:</p> <ol> <li>最简安装的步骤</li> <li>主流程功能的使用</li> <li>开发环境的搭建</li> <li>开发注意事项 <ol> <li>包含组件的架构图</li> <li>包含代码的分层目录说明</li> </ol></li> </ol> <p>这个文档需要由项目的负责人编写,在后续由项目的新人实践的过程中逐步完善。</p> <p>当然这都是比较理想的情况,实际情况中,我们可能遇到的项目可能根本就没有文档,然后只有一个代码仓库的情况,那怎么才能快速上手呢?其实还是按照Quick Start的思路,只不过需要自己来摸索来了。</p> <p></p> <h3 id="1-安装">1. 安装</h3> <p>我相信不管什么项目,或多或少都有一些如何安装的文档,如果连一点这样的文档都没有,那就需要问其它同事了,如果连问都问不出来,那恭喜你赶快跑路吧,😂</p> <p>因为我们是初次接触项目,不必搞懂安装的每个步骤,每个配置的含义,目标仅仅是把项目跑起来,能用起来,所以这里的目标就是安装最简化的方式把项目跑起来。当前的后端项目一般采用容器交付的形式,一般就交付镜像与helm chart,这种方式的安装理论来说是非常简单的,一条helm install 的命令就可以搞定。所以我们在交付的的时一定要在默认的配置中写好最简化的配置,这样新人就可以直接用默认的配置来安装。</p> <h3 id="2-功能体验">2. 功能体验</h3> <p>安装好之后,我们就可以从产品的角度开始体验项目的相关功能,这里我们要聚焦到项目的核心功能上,跑完整个主要流程,不要在意细节,也不要急于把体验的功能与代码实现结合起来,我们只要梳理功能流程,数据的流转,数据的建模。然后有经验的程序员往往会在这个过程中思考,这个功能如果是我来做,我会怎么做。在这个过程中一定要有自己的输出,比如:</p> <ol> <li>功能清单</li> <li>功能流程图</li> <li>数据模型</li> <li>数据流转图</li> </ol> <p>如果有思考如何实现,还可以记录这些:</p> <ol> <li>技术选型</li> <li>技术方案</li> <li>架构设计</li> </ol> <p>在体验过程中如果发现了问题,也需要记录下来,还有自己对项目的疑问,为什么要这么设计,这个功能解决的什么实际问题等等。</p> <p>但是我们需要注意的是避免陷入细节,有些东西没搞懂就没搞懂,没有关系,我们是一个新人,搞不懂也正常,把没搞懂的问题记录下来,后续再逐步熟悉。</p> <h3 id="3-组件拆解与用途分析">3. 组件拆解与用途分析</h3> <p>在功能体验完成后,我们就有了自己的对与项目的思考,也有了一些疑问,然后我们就可以从项目的依赖的各个组件来逐步搞清楚这些问题。比如项目中用了什么数据库,用了什么缓存等等。然后我们可以对照我们自己的分析来看看项目是不是真的就是想我们想想的来实现的,数据库的表设计是不是也跟我们的数据建模一致。经验丰富的程序员往往很快就能猜出项目的主要组件依赖,快速画出一个简单的架构图。</p> <h3 id="4-项目代码分析">4. 项目代码分析</h3> <p>搞明白各个组件的用途后,我们需要的是单刀直入,从主要功能流程出发,找到对应代码的入口,然后按照我们整理的功能流程,梳理出代码流转的过程,在这个过程中我们就可以大体分析出项目代码中目录的分层,各个模块的功能了。我们还可以借助工具来绘制代码的依赖图,这样可以帮助我们快速的理解代码。</p> <p>在阅读的过程中代码本身可能也会有我们看不惯的地方,这个时候我们需要把代码切出一个分支来,专门用来记录自己阅读时写的注释以及一些觉得要重构的点,但是千万不要直接开始动手,一定只是记录,打上标记,比如:</p> <pre><code>// TODO: 这里有待重构 // ? 为什么这里用的是这种方式 </code></pre> <p>等等,也是把自己的疑问记录下来,后续有需要可以带着疑问去问同事。</p> <p>在代码阅读的过程中,切记不要陷入细节,不要陷入细节,不要陷入细节。我们只需要搞清楚代码流转的过程即可,至于代码的实现细节,我们后续在解BUG的过程中再去逐步熟悉。</p> <h3 id="5-从小问题入手-逐步上手">5. 从小问题入手,逐步上手</h3> <p>入手项目的初期不要好高骛远,我们从解小的BUG开始熟悉怎么正确的提交第一个PR,在这个过程中我们可以学习项目的规范,正确的写单元测试,然后解BUG的过程中我们不要看哪里的代码就只是哪里的代码,我们还需要关注模块的边界,了解整个模块都有一些什么功能。很多的BUG往往不是主要功能流程中的问题,在处理这些小的BUG的时候正是我们去了解其它非主要流程功能的好机会。</p> <h3 id="6-新功能与重构">6. 新功能与重构</h3> <p>当我们对项目有一定的了解后,就可以做一些新功能了,然后在规划新功能时,往往我们需要对涉及到已有的代码做一些重构,我们需要把握代码重构的一个度,一定是有限处理对我们写新功能有阻碍的代码,然后需要考虑功能的后续迭代与未来的演进来重构。渐进式的重构,不要上来就搞大的,我相信很多程序员都希望能把代码写的跟符合自己的审美,但是初期还是要以功能为主,再实现功能的同时小步渐进式的重构。重构有很多技巧,我们可以把我们改不动的代码做隔离,先保证能跑起来,然后再完善。</p> <h3 id="总结">总结</h3> <p>初入一个项目,我们可能会遇到很多很多问题,但是没有关系的,我们需要做的是把这些疑问都记录下来,然后带着疑问慢慢去熟悉,熟悉一个项目也需要一个过程,不要着急,1个星期不行,就1个月,遇到困难就及时寻求帮助。前人种树,后人乘凉。如果前人没能帮你种好树,那我们就把我们熟悉项目的过程整理成文档,留给后人,这样后人就可以少走很多弯路。</p> 我的2024 https://zhu327.github.io/2024/12/31/%E6%88%91%E7%9A%842024/ Tue, 31 Dec 2024 10:55:52 +0800 https://zhu327.github.io/2024/12/31/%E6%88%91%E7%9A%842024/ <p>今天是2024年的最后一天,我有这一下午的空闲时间,所以就让我来絮絮叨叨的写写我这神奇的2024年吧。</p> <p>我是龙年出生的,所以2024年是我的本命年,年初的时候,老婆还有姐姐就给我准备了红内裤,红袜子。由于红袜子太过显眼了,所以这一年我的内裤都是红色的,哈哈。本命年,犯太岁,这一年工作上,心态上,都有一些变化,就让我在本命年的最后一天瞎写写吧。</p> <h3 id="我自己">我自己</h3> <p>我是80年代末生人,出生在一个小镇,是标准的小镇做题家。从高中开始基本就不怎么回家了,然后上大学,跑去了云南,双非一本,学的是热能与动力工程。我的同学大都找到了水电厂,火电厂的工作。工作好的去了三峡,一般就去了云南一些比较偏僻的电厂。而我呢毕业就失业了,后来找到一个海南的小电厂。工作2年后,我就来到了深圳,在躺了好几个月后,经一个亲戚的介绍到一个华为的软件外包公司学习做软件测试。</p> <p></p> <p>软件测试本身是没什么门槛的,然后我自认为还有点小聪明,也由于亲戚的安排,我所在的项目并不是纯粹的手工测试。而是偏向自动化的TCP/IP协议栈的测试,这个测试需要我写一些Lua脚本来模拟发送一些数据包来做测试。虽然这不是一个开发的工作,但是在这个过程中我还是学到不少东西,基本的协议栈,socket,还有一些基础的编程语言方面的知识。</p> <p>后来又换了别的项目,做GUI跟网页的功能测试,我接手这个项目的测试的时候,里面还维护了一堆Java写的基于一个IBM的测试框架的自动化测试用例。这可难倒我了,以至于大半年的时间我都没跑过这个自动化的测试,一直都是手动测试。想想不是办法,还是搞搞自动化的测试吧,Java是一时半会学不会的,不如自己学一学Python自动化吧,就这样我接触到Python编程。我依然记得在某一天我用Python写了一个自动匹配关键字的工具用来做安全检查后,获得了华为的持续集成奖。</p> <p>就这样我在这个软件外包公司做测试做了4年,然后我决定还是找一份Python开发的工作吧,于是在学习了Django开发后,我就裸辞了。</p> <p>在2015年的那个激荡的万众创业,万众创新的2015年,只要你会写点代码,就能找到工作。于是我入职了一家很小的公司做Python后端开发,然后半年之后,我就被优化了。这个时候我确实怀疑过自己,我是不是真的适合做开发呢,我能拿出手的项目只有我在github上写的一个Django的论坛。</p> <p>然后就又开始找工作了,不是很顺利,直到2015年下半年的某一天,我收到了一封邮件,嗯,一家我没有投递的公司邀请我去面试。后来才从这家公司的CTO口中得知,他是在看到我的github才邀请我去面试的,然后我就入职了这家公司,继续做Python的后端开发。在这家公司我又找到了自信。然后在这里我发现我做测试的经历还是有用的,毕竟我的自测做的比较细致,以至于我的效率以及质量都远超其他同事。然后由于效率的提升,很快我就开始负责更基础的框架开发,引入了DRF框架,还尝试做了RPC框架,然后又引入了API网关,认识了Openresty,你看,Openresty又回到了我最初做测试的老本行Lua上面。</p> <p>由于在github上面尝到了甜头,后续我又在github上写了一些Openresty的小项目,也有Python RPC相关的项目,然后也写了一些我觉得还行的博客文章。我感觉我的能力还是又比较大的提升的,我想我是不是再出去找找机会呢?所以我又开始面试了,不出意外的还是被打击到了,无非是因为非科班出生,然后基础很差。有一个比较nice的面试官推荐我去读一读深入理解计算机系统,然后我就买了这本书。年轻的时候还是比较要强的,不喜欢认输,所以花了好几个月死啃这本书,然后再回顾自己的在工作中点点滴滴,确实对编程这个工作有了更多的理解。再然后我就买了更多的书,在douban上找评分高的书,补齐了很多基础知识。</p> <p>时间来到了2018年,很幸运的是我通过了腾讯的面试,这里面有我读书的能力提升,也有不停在github上写项目的努力,还有我那几年积累下来的工作经验。</p> <h3 id="腾讯">腾讯</h3> <p>初入腾讯,我是在一个运维组里的唯一的开发,我接的需求都是我的运维同事通过开发工具来改善他们的运维工作。这期间有一个直播推流相关的需求对我来说算是一个契机,促成了我从Python程序转向Golang程序员。在这个组里做了一年后,我觉得我做的的事情都不具备持久性,没有一个清晰的未来,所以我决定换一个组,于是我来到了蓝鲸的开发组。</p> <p>在腾讯作为一个非科班出身的新人,我并没有觉得自己能做很久,我脑子里一直有一个念想就是做个几年,有了大厂光环,然后再出去作为一个架构师找一份新的工作。但是我在蓝鲸确实认识了一群nice的同事,然后这一做就是5年。这期间我一直专注在一个项目上,可以说成为了领域内的专家。持续的一直做一件事情有好处,也有坏处,好处是你可以在这个领域内做到极致,坏处是你很难跳出这个领域。</p> <p>在大厂,每半年一次的考核,催促着我成长,沟通能力,项目管理能力,架构能力,规划能力等等,我都有实实在在的成长。然而在本命年的这一年,我还是觉得我可以离开了,毕竟互联网35岁的危机,年纪越大感受越深。</p> <h3 id="新工作">新工作</h3> <p>虽然顶着大厂光环,但是在2024年36岁的年纪,找工作还是很困难,面试了很多,只要涉及写算法什么的基本都挂了,然后我就降薪入职了现在的公司。我有了更多的时间,然后写了更多的文章,也学习了Rust编程,新的Rust的项目也都放到了github上。我不知道自己的职业生涯还能走多远,但是我觉得我依然热爱编程。</p> <p>随着AI编程应用越来越多,我觉得对于头部的程序员来说,他们的优势会越来越大,因为AI会极大的提升他们的效率,这对中下部的程序员可能是一个灾难。可能会因为这个整个市场的行情会越来越差,所以我个人觉得持续学习,终身学习在这个时代是必须的。</p> <h3 id="生活">生活</h3> <p>3月的时候跟老婆来了一次广西自驾游,从深圳出发,开着我的电车第一次长途自驾,感谢老婆的辛苦做了攻略,在广西边境看了大瀑布,也第一次体验了溶洞速降,还看到了美丽梦幻的三门海。也去了一些小众的景点。在广西的山间小路悬崖边小心的开着车,在溶洞里面听着水流的声音,看着头顶的灯光,那种感觉真的很棒。</p> <p>7月的时候,正式从腾讯离职了,然后也没找到工作,就在家里躺平静静的思考了,关于工作不焦虑是假的,但是生活也是要继续的。新的工作开始后,我有了更多的时间陪小孩,看着小孩的成长,确实又感觉生活不一样了,小孩的需求没那么多,更多是我都是作为一个捧哏的角色,看着她玩耍,配合她的各种需求。</p> <p>关于未来,世界变化很快,我也不知道未来会怎么样,在深圳还没有买房,没啥压力,在广西游的过程中有个小小的种子种在了心里,什么时候能在广西找个山好水好的地方过上慢生活呢。</p> <h3 id="结尾">结尾</h3> <p>回头看2024年,这一年过得挺特别的,有变化也有成长。无论是工作上的转折,还是生活中的点滴,我都在慢慢适应、慢慢调整。虽然未来会怎样还不好说,但我想继续保持学习的习惯,保持对生活的热情,也尽量多陪陪家人。生活总是这样,充满未知,但也正因为如此,每一天都值得认真过好。</p>