<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>之卡</title><description>No description</description><link>https://www.corgi37.top/</link><language>zh_CN</language><item><title>🌐Graph Engineering 是什么？一文讲透多 Agent 图式编排</title><link>https://www.corgi37.top/posts/graph-engineering-duo-agent-bianpai/</link><guid isPermaLink="true">https://www.corgi37.top/posts/graph-engineering-duo-agent-bianpai/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;还记得吗？上个月才刚出 loop engineering 这个新词。&lt;/p&gt;
&lt;p&gt;结果 AI 进步速度超乎想象，我又看到了新词，叫「&lt;strong&gt;graph engineering&lt;/strong&gt;」。&lt;/p&gt;
&lt;p&gt;事情的起因是，前几天 OpenClaw 小龙虾创始人说了一句「我们还在聊 loop，还是已经切到 graph 了？」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723190550837.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后就有其他大佬直接跟帖喊了一句「loop engineering 已死，graph engineering 永生！」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723190634118.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;要知道，上次喊 loop engineering 的也是小龙虾创始人，我算了一下，两次相隔才 40 天，又搞出新东西了，不愧是&lt;strong&gt;万词王&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此时此刻，我的心情如下，真累了，快追不动了，别造新词了好嘛。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723190842308.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以我悟了，在 AI 圈，只要学得足够慢，就可以不用学了。&lt;/p&gt;
&lt;p&gt;话虽如此，graph engineering 到底是啥牛鬼蛇神，今天还是来聊一聊。&lt;/p&gt;
&lt;h2&gt;什么是 Graph Engineering？&lt;/h2&gt;
&lt;p&gt;先说清楚一个词。graph 直译过来是「图」，是数据结构里「节点加连线」的那个图，节点叫 Node，节点之间的那根连线叫边（Edge），下面我统一写 graph。&lt;/p&gt;
&lt;p&gt;graph engineering 是在干嘛呢？你想想自己公司是怎么运转的，就好理解了。&lt;/p&gt;
&lt;p&gt;一家公司不会让一个人从头到尾包办调研、开发、测试、上线，哪怕这个人是全才也不行。公司的做法是拆分职责，每个人负责一个环节。有些环节必须等上一步完成后才能开始，有些环节则可以同时推进。哪个环节出了问题，也能很快定位到对应的负责人。&lt;/p&gt;
&lt;p&gt;复杂任务也是一样。它通常包含多个步骤，有的存在先后依赖，有的互不影响，可以并行执行。&lt;/p&gt;
&lt;p&gt;graph engineering 干的就是这件事。它按照任务之间的依赖关系，用 graph 来编排整个流程，该串行的串行，该并行的并行，每一块交给一个职责明确的节点去完成。&lt;/p&gt;
&lt;p&gt;说白了，它不是发明了什么新玩法。它是提醒你，别再把一件本来可以并行、分支执行的任务，强行改成全程串行执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/e52a2d38_linearized-vs-natural-task-shape.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;讲到这，有的同学就会问了，那 loop 被淘汰了？&lt;/p&gt;
&lt;p&gt;别慌，恰恰相反。你可以把一个 loop 理解成一张最小的 graph，只有一个节点（Node），一条边（Edge），而且这条边指回自己。graph 上每个 agent 节点的内部，跑的还是那个熟悉的思考循环。&lt;/p&gt;
&lt;p&gt;所以这俩根本不冲突。loop engineering 解决的是单个 agent 如何反复思考、调用工具和修正结果，graph engineering 解决的是多个执行单元如何拆分、并行、汇合和返工。简单说，前者管节点内部，后者管节点之间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/adfd1df4_loop-as-smallest-graph.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可能又有的同学就问了，任务之间的依赖关系我懂了，可我就在一个对话里，让同一个 agent 一步一步做，不也能做完吗？&lt;/p&gt;
&lt;p&gt;能是能，但如果只是让一个 agent 在一段长对话里从头跑到尾，把一件本来可以并行、分支执行的任务改成全程串行执行，就会撞上几个躲不掉的麻烦。平时做点小任务没事，任务一放大，马上全冒出来。&lt;/p&gt;
&lt;p&gt;先说最直接的，慢。全程串行执行，就只能一步一步来，明明后端、前端、测试能三路一起跑，也得排着队一个干完再干下一个。&lt;/p&gt;
&lt;p&gt;再一个，如果没有额外设计状态和存档机制，这种串行流程跑下来，全部家当就只剩一长串聊天记录。执行到第 40 步突然中断怎么办？要么从头重跑，要么你自己钻进上下文里一点点翻，看看还能抢救出什么。&lt;/p&gt;
&lt;p&gt;暂停也麻烦。这种从头跑到尾的执行方式，如果没有额外保存进度，中间很难停下来等人工审批，隔天再从断点接着干。要么一直耗着，要么中断后重新来。&lt;/p&gt;
&lt;p&gt;最要命的是，单个 agent 串行执行很难表达复杂分工。&lt;/p&gt;
&lt;p&gt;你想要「一个负责规划的，带三个干活的，再来一个专门挑刺的」，这个念头一冒出来，串行流程就开始吃力了。它擅长回答「下一步做什么」，却不擅长同时表达「谁来做、哪几个人一起做、做完在哪里汇合」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/bf7dcc44_four-limitations-of-linear-flow.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我看到一个说法特别贴切，这么跑任务，就像系统有很强的执行能力，却没有统一调度，每次只能处理一件事，完成后才能进入下一件。&lt;/p&gt;
&lt;p&gt;说白了，它缺的不是脑子，而是任务调度和流程管理能力，把任务分给多个 agent 同时执行、记录每一步的进度、执行中断后还能恢复。&lt;/p&gt;
&lt;p&gt;这两年模型完成单个步骤已经越来越可靠，真正容易翻车的，是「一百个步骤怎么协作起来」，而 graph engineering 补上的，就是这层任务调度和流程管理能力。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Graph Engineering 的工作原理是什么？&lt;/h2&gt;
&lt;p&gt;其实，一张 graph 拆开就三样东西，&lt;strong&gt;节点（Node）、边（Edge）、状态（State）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;graph 工程的工作原理全藏在这三样里，我们一个个说。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/41a0063d_node-edge-state-overview.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一样，节点（Node），就是干活的那个&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个节点可以是一个跑着完整思考循环的 agent，也可以是一段按照固定规则运行的普通代码，或者一次数据库查询。甚至，人也行。对，人也是节点，审批这个环节就是一个人类节点：前面的任务流进来，你做出决定，后面的任务再继续往下走。&lt;/p&gt;
&lt;p&gt;好节点的标准特别朴素，它只干一件事，单独就能测，哪天想换掉它，别的节点也不受牵连。一个节点要是身兼五职，那它就不是节点了，它就是又一个大 loop。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/95f9742f_node-types-one-job.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二样，边（Edge），负责决定下一步执行哪个节点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;设计一条边（Edge），其实是在回答两个问题：下一个节点由什么决定，以及是否要根据结果选择不同分支。&lt;/p&gt;
&lt;p&gt;先看由什么决定下一个节点。&lt;/p&gt;
&lt;p&gt;可以由提前写进代码的固定规则决定，这叫确定性边（Deterministic Edge），比如测试全部通过就进入部署，根本用不着模型掺和。&lt;/p&gt;
&lt;p&gt;也可以交给模型判断，这叫模型决策边（Model-decided Edge），比如一个工单进来，该派给处理退款的节点还是处理投诉的节点。&lt;/p&gt;
&lt;p&gt;设计的功夫就在分清哪条边（Edge）该写死、哪条该交给模型。能写死的尽量写死，把模型的判断能力留给少数确实需要理解语义的分支。&lt;/p&gt;
&lt;p&gt;再看流程是不是固定的。固定流程总是进入同一个节点，条件边（Conditional Edge）则会根据上一步的结果分叉，比如测试通过就进入下一个节点，测试失败就返回返工节点。&lt;/p&gt;
&lt;p&gt;所以，条件边和前面两种不是并列关系。一条条件边既可以由代码判断，也可以由模型判断。比如「测试通过就审批，失败就返工」，是一条由代码判断的条件边；「根据工单内容分到退款或投诉节点」，则是一条由模型判断的条件边。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/c7c3903c_three-types-of-edges.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三样，状态（State），一个字段结构固定的对象&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;任务按照 graph 定义的顺序依次经过各个节点，执行到哪、每一步产出了什么、消耗了多少 token，这些信息总得有个地方存着。这个地方就是状态。你可以把它理解成一张字段固定的任务进度表，也可以把它理解成一份结构化的 JSON：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;backend&quot;: {&quot;status&quot;: &quot;done&quot;, &quot;artifact&quot;: &quot;api/export.js&quot;},
  &quot;frontend&quot;: {&quot;status&quot;: &quot;running&quot;},
  &quot;tests&quot;: {&quot;status&quot;: &quot;pending&quot;},
  &quot;tokens_used&quot;: 12000
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;谁要看进度，读这个对象就行，不用再钻进一大段聊天记录里翻。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/a89004c5_structured-state-vs-chat-log.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;工程实现时，通常会在一个节点执行完成后，把最新状态保存下来，相当于存一次档。&lt;/p&gt;
&lt;p&gt;当然，用 graph 来编排任务，并不会自动存档。还得靠运行框架，或者你自己把状态写进数据库、文件这些地方。&lt;/p&gt;
&lt;p&gt;有了存档，流程中断就完全不是一回事了。以前中断叫「这一趟白跑了」，现在中断叫「读档，重试那一个节点」。打游戏你就懂，没有存档点的游戏，死一次回出生点，谁受得了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/dd1df2a7_state-checkpoints-retry.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说到底就是，节点（Node）负责干活，边（Edge）负责决定下一个执行哪个节点，状态（State）负责记录整个流程的进度和结果，每个节点完成后再把它保存下来。&lt;/p&gt;
&lt;p&gt;三样凑齐，前面说的那些问题也就分别有了解法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;想并行？让一个节点同时分出三条边（Edge），启动三个节点，这个动作叫分支并行（fan-out），干完再汇合（fan-in）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;想暂停等审批？在流程中增加一个等待人工确认的节点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;想执行中断后还能接着跑？不用从头来，读上一个存档点就行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对了，图的状态还是个结构化的对象，每个节点的产出、测试结果、消耗的 token 都是一个个明确的字段。正因为它结构化、程序能直接读，前面说的存档和返工才转得起来。条件判断读取测试结果字段后，就知道该继续执行还是返回返工节点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/3ed0af2e_requirements-to-graph-features.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Graph Engineering 怎么上手实操？&lt;/h2&gt;
&lt;h3&gt;真的需要用 graph 来编排吗？&lt;/h3&gt;
&lt;p&gt;动手之前先泼一盆冷水，你手上大部分的任务，按照顺序执行就够了，犯不着专门用 graph 来编排整个流程。&lt;/p&gt;
&lt;p&gt;写个脚本、修个 bug、做个简单调研，这些任务的前后步骤很明确，按照顺序执行就行，强行用 graph 来编排纯属给自己找罪受。一个人一下午能干完的任务，没必要开项目启动会。&lt;/p&gt;
&lt;p&gt;什么样的任务适合用 graph 来编排？我总结下来是这么几个。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务能拆成互相独立的子任务，而且需要缩短总耗时，不能全部串行执行&lt;/li&gt;
&lt;li&gt;不同环节需要不同的模型、工具、权限，比如干活的节点可写、审查的节点只读&lt;/li&gt;
&lt;li&gt;流程中间有必须人工确认的环节，上线前必须有人点头&lt;/li&gt;
&lt;li&gt;任务执行时间长、分支多，你需要随时查看每个分支的进度、产物和 token 消耗，而不是翻一长串聊天记录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一条都不符合，继续采用串行流程就行。符合两三条，再往下看。&lt;/p&gt;
&lt;p&gt;对了，还有个前提。先确保每个节点都能稳定完成一件事，再谈怎么把多个节点编排到一起，顺序不能反。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/9d228060_graph-upgrade-checklist.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;怎样用 graph 编排一个开发需求？&lt;/h3&gt;
&lt;p&gt;好，假设你的任务符合其中两三条。拿个开发场景走一遍你就明白了。&lt;/p&gt;
&lt;p&gt;你接了个需求，给系统加一个数据导出功能。后端要写导出接口，前端要加导出按钮和进度条，测试要补对应的用例。只要先约定好同一份 API 契约，三块任务就可以各自开工，最后再合到一起验收。&lt;/p&gt;
&lt;p&gt;要是采用串行方式处理，就得先写后端，再写前端，再补测试。中间哪一步引入的问题，都会继续影响后面的环节，越到后面越乱。&lt;/p&gt;
&lt;p&gt;改用 graph 来编排，思路完全不一样。&lt;/p&gt;
&lt;p&gt;第一个节点是规划节点，把需求拆成后端、前端、测试三份任务，同时确定一份三方共用的 API 契约，再一起写进状态对象。这一步用模型，因为拆任务和制定契约都需要判断。&lt;/p&gt;
&lt;p&gt;然后进入并行分支，三个开发节点同时启动，各自在独立的 &lt;code&gt;git worktree&lt;/code&gt; 里干活，互相不碰对方的文件。每个节点内部就是一个标准的编码 loop，领任务、写代码、自测、提交。&lt;/p&gt;
&lt;p&gt;三个分支完成后，汇合到集成节点，合并三个分支，跑全量测试。&lt;/p&gt;
&lt;p&gt;接下来是条件边（Conditional Edge），也是我觉得整个设计里最妙的地方。测试全部通过，继续往下执行。测试失败，就看是哪个模块的用例没有通过，把状态对象里对应的任务标记成返工，再交回那个开发节点重做。&lt;/p&gt;
&lt;p&gt;注意，只有出问题的那一路需要返工，另外两路不需要重跑，直接保留已有结果，因为每个节点完成后都保存过状态。&lt;/p&gt;
&lt;p&gt;测试全部通过之后，流程停在最后一个节点上，等待人工确认。这个节点不执行自动化任务，而是暂停流程，等待你本人确认。&lt;/p&gt;
&lt;p&gt;你看一眼 diff，点个头，graph 才走完最后一条边（Edge），合入主干。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/85360be5_data-export-agent-graph.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;整个过程里，状态对象会持续记录三份任务、每一路的进度和产物路径、测试结果，以及消耗了多少 token。任何时刻你想知道进展，看状态对象就行，不用翻任何聊天记录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/f20e2edb_structured-state-ledger.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;用 LangGraph 怎么写？&lt;/h3&gt;
&lt;p&gt;用 LangGraph 实现前面的开发流程，最重要的不是记住每个 API，而是看懂节点之间怎么连接。&lt;/p&gt;
&lt;p&gt;下面这段代码省略了每个节点内部的具体实现，只保留整张 graph 的骨架。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langgraph.graph import StateGraph, START, END

g = StateGraph(DevState)

# 注册节点
g.add_node(&quot;plan&quot;, plan_task)
g.add_node(&quot;backend&quot;, backend_dev)
g.add_node(&quot;frontend&quot;, frontend_dev)
g.add_node(&quot;tests&quot;, test_dev)
g.add_node(&quot;integrate&quot;, integrate_code)
g.add_node(&quot;rework&quot;, rework_code)
g.add_node(&quot;approval&quot;, human_approval)

# 从规划节点开始
g.add_edge(START, &quot;plan&quot;)

# 规划完成后，三个节点并行执行
g.add_edge(&quot;plan&quot;, &quot;backend&quot;)
g.add_edge(&quot;plan&quot;, &quot;frontend&quot;)
g.add_edge(&quot;plan&quot;, &quot;tests&quot;)

# 三个节点都完成后，再进入集成节点
g.add_edge(
    [&quot;backend&quot;, &quot;frontend&quot;, &quot;tests&quot;],
    &quot;integrate&quot;
)

# 根据测试结果决定下一步
g.add_conditional_edges(
    &quot;integrate&quot;,
    check_tests,
    {
        &quot;pass&quot;: &quot;approval&quot;,
        &quot;fail&quot;: &quot;rework&quot;
    }
)

# 返工完成后重新集成
g.add_edge(&quot;rework&quot;, &quot;integrate&quot;)

# 人工确认后结束
g.add_edge(&quot;approval&quot;, END)

app = g.compile()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看第一行，&lt;code&gt;StateGraph(DevState)&lt;/code&gt; 创建了一张 graph，&lt;code&gt;DevState&lt;/code&gt; 就是前面讲过的状态对象。每个节点都可以读取它，也可以把自己的执行结果写回去。&lt;/p&gt;
&lt;p&gt;接下来的 &lt;code&gt;add_node&lt;/code&gt; 是把规划、后端、前端、测试、集成、返工和人工确认注册成节点。节点内部可以是一段普通代码，也可以是一个完整的 agent。&lt;/p&gt;
&lt;p&gt;从 &lt;code&gt;START&lt;/code&gt; 进入规划节点后，流程会同时启动后端、前端和测试三个节点。&lt;code&gt;add_edge([&quot;backend&quot;, &quot;frontend&quot;, &quot;tests&quot;], &quot;integrate&quot;)&lt;/code&gt; 表示必须等三个节点都完成，才能进入集成节点。&lt;/p&gt;
&lt;p&gt;集成完成后，&lt;code&gt;add_conditional_edges&lt;/code&gt; 会读取测试结果。测试通过就进入人工确认节点，测试失败就进入返工节点。返工完成后重新回到集成节点，再跑一次测试。&lt;/p&gt;
&lt;p&gt;最后的 &lt;code&gt;compile()&lt;/code&gt; 会把前面注册的节点和边组装成一张可以执行的 graph。&lt;/p&gt;
&lt;p&gt;这样一来，代码和前面的开发流程就能一一对应。规划、并行开发、汇合集成、失败返工和人工确认，在代码里都能直接找到。&lt;/p&gt;
&lt;h3&gt;用 Claude Code 怎么玩？&lt;/h3&gt;
&lt;p&gt;看到这估计有同学就问了，我平时用的是 Claude Code，不想专门学 LangGraph，能不能实操 graph engineering？&lt;/p&gt;
&lt;p&gt;能。Claude Code 已经提供了 subagent、hook 和 workflow 这些组件，可以直接用它们实现 graph 编排。&lt;/p&gt;
&lt;p&gt;一次 subagent 执行可以充当一个节点。在 &lt;code&gt;.claude/agents/&lt;/code&gt; 目录下，一个 md 文件定义一个专职 agent，各自写清楚职责、能用哪些工具、用哪个模型。干活的给写权限，审查的只给只读。&lt;/p&gt;
&lt;p&gt;hook 可以在特定时机强制执行固定检查。比如你可以配置一个 hook，在 agent 结束任务前自动运行测试，测试没有通过就继续修改。&lt;/p&gt;
&lt;p&gt;那 graph 本身呢？新版 Claude Code 内置了 &lt;strong&gt;workflow 编排能力&lt;/strong&gt;。这里的 workflow，可以理解成一段负责拆任务、派 agent、等结果和处理分支的调度脚本。你把目标说清楚，它自己会写出这段脚本，再照着脚本派出一队 subagent 去执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/2522be5f_claude-code-to-graph-mapping.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也就是说，脚本不用你动手，你要做的就是在对话框里把目标讲清楚，比如这样一句话。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用 workflow 帮我开发一个记账网站，拆成后端接口、前端页面、测试用例三块并行开发，做完合并跑全量测试，测试全部通过后叫我确认&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我拿这句话实际跑了一遍。它接到后没有闷头就干，先把打算怎么拆讲清楚，还主动写了一份 API 契约，让三路 agent 都按同一份契约来开发，省得合并的时候各说各话。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723225148499.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接着它把整个 workflow 的编排方案展示出来等我确认。整个流程分成两个阶段，第一阶段由三个 agent 并行开发，第二阶段负责集成和测试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723225235430.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我确认方案后，workflow 就在后台运行起来了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723225504189.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;执行过程中，随时可以输 &lt;code&gt;/workflows&lt;/code&gt; 看后台的任务进度，三路 agent 各自跑到哪一步、消耗了多少 token，一目了然。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723225528714.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跑完之后，它跟我说 42 个测试用例全部通过，喊我来验收。我让它把项目跑起来，看看实际效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723225928954.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;效果是这样的，整体确实不错，能直接用起来，页面也设计得挺像样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/20260723230138_rec_.gif!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后我回复它「没问题」，这一轮协作就算交付完成了。注意这最后一步，就是前面说的人类节点。我确认之后，流程才通过最后一条边（Edge），把代码合入主干。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723230353586.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;除此之外，Claude Code 内置的 &lt;code&gt;/deep-research&lt;/code&gt; 命令，本身就是一个已经投入实际使用的 graph 工作流。它会拆解问题、并行搜索、抓原文、交叉验证，最后合成一份带引用的报告。&lt;/p&gt;
&lt;p&gt;比如你想追一追最近模型圈的动态，可以这么问。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;/deep-research 最近一个月国内和海外大模型的重要动态，国内重点看 DeepSeek、通义千问、Kimi 这些，海外重点看 OpenAI、Anthropic、Google，按重要程度排序，标注信源&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个调研任务很适合拆成多个方向并行搜索，我也实际跑了一次。它接到后先把这次调研拆成了五个阶段：拆解、搜索、抓取、验证、综合。其中搜索这一步又拆成 5 个角度并行去查，国内头部、海外三巨头、其他海外玩家等等，每个 agent 分别负责一个方向。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723230626427.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同样输 &lt;code&gt;/workflows&lt;/code&gt;，就能在进度树里亲眼看到一个节点分出多条边（Edge）的样子。到了搜索这一步，它同时启动 5 个 agent，每个 agent 负责搜索一个角度。这正是前面讲的分支并行，只不过这次可以直接在屏幕上看到它们同时执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260723230709165.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;等五个阶段全跑完，它就把结果整理出来了，每条结论后面都老老实实标着信源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/20260724100346_rec_-convert.gif!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;两个例子背后有一个共同点。主会话只负责拆解任务、启动 subagent、等待结果和最终汇总，不再亲自完成中间的每一个步骤。&lt;/p&gt;
&lt;p&gt;具体的开发和搜索工作，会在各个 subagent 的独立上下文中进行。它们产生的大量中间推理、工具输出和临时结果，不需要全部塞回主会话，只需要返回后续流程真正用得上的结果。&lt;/p&gt;
&lt;p&gt;无论是记账网站的三个开发节点，还是 deep-research 的多个搜索节点，都能在并行执行的同时，避免主会话被大量中间过程占满。&lt;/p&gt;
&lt;p&gt;这也是 graph 编排的一个重要价值。它不仅让任务跑得更快，也把调度过程和具体执行过程分开了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Graph Engineering 和 LangGraph 有什么区别？&lt;/h2&gt;
&lt;p&gt;看到这估计有同学要问了，这不就是 LangGraph 干的事吗？连名字都撞上了。&lt;/p&gt;
&lt;p&gt;从实现方式来看，确实很像。LangGraph 早就在用节点、边和状态来编排 agent 工作流，但两者并不在同一个层级。&lt;/p&gt;
&lt;p&gt;graph engineering 是一种设计方法。它关注任务怎么拆分，哪些步骤可以并行，下一步由什么决定，状态怎么记录，失败之后怎么返工。&lt;/p&gt;
&lt;p&gt;LangGraph 则是实现这套方法的一种框架。它提供了现成的节点、边、状态和流程控制能力，让你可以用代码把设计好的 graph 运行起来。&lt;/p&gt;
&lt;p&gt;不过，做 graph engineering 并不一定要用 LangGraph。你也可以使用 AutoGen、Google ADK、Claude Code 的 workflow，甚至自己写一段调度代码。只要你在按照节点、边和状态来组织多个执行单元之间的协作，本质上就是在做 graph engineering。&lt;/p&gt;
&lt;p&gt;所以，两者更准确的关系是，graph engineering 告诉你应该怎么设计，LangGraph 帮你把这个设计实现出来。&lt;/p&gt;
&lt;p&gt;这套技术并不是最近才出现的。新的是 graph engineering 这个名字，以及大家开始把注意力从「单个 agent 怎么完成一步任务」，转向「多个 agent 怎么共同完成一条复杂流程」。&lt;/p&gt;
&lt;p&gt;也正因为这样，这个词刚开始刷屏，LangChain 官方就发了一篇博客，说这套东西我们已经做了好几年，还顺带调侃 graph engineering 是「X 的 AI 内容工厂」刚造出来的新词。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260724105104165.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;聊到这里，graph engineering 其实没有那么神秘。&lt;/p&gt;
&lt;p&gt;它做的事情，就是把一个复杂任务拆成多个职责明确的节点，再按照任务之间的依赖关系，安排它们串行执行、并行执行、条件分支和失败返工。节点负责完成任务，边负责决定下一个执行哪个节点，状态负责记录整个流程的进度和结果。&lt;/p&gt;
&lt;p&gt;当然，不是所有任务都值得用 graph 来编排。任务很小、按照顺序就能完成的，直接交给一个 agent 跑完就行。只有当任务可以拆成多个并行分支、中间需要人工确认、执行中断后还要继续，或者需要长期追踪进度和产物时，graph 编排的价值才会真正体现出来。&lt;/p&gt;
&lt;p&gt;真正值得关注的，也不是 graph engineering 这个新名字，而是它背后的问题。随着 agent 能完成的任务越来越复杂，我们要解决的已经不只是「这一步怎么做对」，而是「几十个步骤怎么稳定地协作起来」。&lt;/p&gt;
&lt;p&gt;名字是新的，问题不是新的。就算过几个月 graph engineering 又被下一个新词替代，节点、边、状态和任务调度这些东西，依然不会消失。&lt;/p&gt;
&lt;p&gt;不过按 AI 圈现在这个造词速度，我估计过不了多久，又会冒出一个听起来更厉害的新词。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;We Are Entering the Graph Engineering Phase：&lt;a href=&quot;https://www.drjoshcsimmons.com/writing/we-are-entering-the-graph-engineering-phase&quot;&gt;https://www.drjoshcsimmons.com/writing/we-are-entering-the-graph-engineering-phase&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3 Years of Graph Engineering with LangGraph（LangChain 官方博客）：&lt;a href=&quot;https://www.langchain.com/blog/3-years-of-graph-engineering-with-langgraph&quot;&gt;https://www.langchain.com/blog/3-years-of-graph-engineering-with-langgraph&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Is Graph Engineering Real?（Turing Post）：&lt;a href=&quot;https://www.turingpost.com/p/is-graph-engineering-real-why-everyone-is-talking-about-it&quot;&gt;https://www.turingpost.com/p/is-graph-engineering-real-why-everyone-is-talking-about-it&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/engineering/graph-engineering.html&quot;&gt;agent/engineering/graph-engineering.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🕸️GraphRAG 和 LightRAG 详解：原理、对比与选型</title><link>https://www.corgi37.top/posts/graphrag-he-lightrag-xiangjie/</link><guid isPermaLink="true">https://www.corgi37.top/posts/graphrag-he-lightrag-xiangjie/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上一篇讲的是「传统 RAG」（也叫 Naive RAG），核心思路是：把文档切成块（Chunk）→ 做向量化（Embedding）→ 存入向量数据库 → 查询时基于语义相似度检索 Top-K 个文本块 → 塞给大模型生成答案。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776765465476-1e896459-f372-409a-8160-e4301b3d640d.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这套流程很经典，也解决了大模型「闭卷考试」的难题。&lt;/p&gt;
&lt;p&gt;但这套方案在实际企业场景落地的时候，会撞上几堵很硬的墙。&lt;strong&gt;GraphRAG 和 LightRAG 就是为了绕过这几堵墙而出现的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776758959795-4224c89b-b86a-4625-8bce-b4075059f6c8.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以，这篇讲重点介绍下面这些内容，而且也都是我从大厂面经中收集到的面试题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🌟传统 RAG 有什么痛点？&lt;/li&gt;
&lt;li&gt;🌟什么是 GraphRAG？为什么会有 GraphRAG？&lt;/li&gt;
&lt;li&gt;🌟GraphRAG 的工作流程是怎样的？&lt;/li&gt;
&lt;li&gt;GraphRAG 的难点是什么？&lt;/li&gt;
&lt;li&gt;🌟GraphRAG 如何应对增量场景？&lt;/li&gt;
&lt;li&gt;🌟什么是 LightRAG？为什么会有 LightRAG？&lt;/li&gt;
&lt;li&gt;🌟LightRAG 的工作原理是怎样的？&lt;/li&gt;
&lt;li&gt;🌟LightRAG 和 GraphRAG 有什么区别？&lt;/li&gt;
&lt;li&gt;什么场景下需要 GraphRAG？&lt;/li&gt;
&lt;li&gt;什么场景下需要 LightRAG？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整篇文章读完，你就能搞懂这两个当前最火的 RAG 进阶技术，也能知道自己的业务场景到底该选哪个。&lt;/p&gt;
&lt;p&gt;废话不多说，直接开讲。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1、传统 RAG 有什么痛点？&lt;/h2&gt;
&lt;p&gt;要理解 GraphRAG 为什么会出现，咱们得先搞清楚一个问题：&lt;strong&gt;传统 RAG 到底哪里不行？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多同学读完上一篇 RAG 文章，可能会觉得：RAG 不是挺好的吗？能让大模型有「外挂知识库」，能减少幻觉，能回答私有数据的问题……听起来已经很够用了。&lt;/p&gt;
&lt;p&gt;但问题是，&lt;strong&gt;「够用」和「能打」是两码事&lt;/strong&gt;。你在 Demo 场景下跑一跑，确实没什么问题，但一旦真正拿到企业的复杂场景去用，你会发现传统 RAG 经常「表现得体面，但答得不对」。&lt;/p&gt;
&lt;p&gt;下面我就用几个真实场景，带你感受一下传统 RAG 会卡在哪里。&lt;/p&gt;
&lt;h3&gt;痛点一：跨文档的「多跳推理」干不了&lt;/h3&gt;
&lt;p&gt;咱们先来看一个真实的业务场景。&lt;/p&gt;
&lt;p&gt;假设你在一家跨国公司做风控，你问 RAG 系统这样一个问题：&lt;/p&gt;
&lt;p&gt;「我们公司哪些欧洲供应商，在过去一年的安全审计中没通过，而且还在处理个人隐私数据（PII）？」&lt;/p&gt;
&lt;p&gt;这个问题要想答对，得同时拿到三份文档里的信息：&lt;strong&gt;供应商档案&lt;/strong&gt;（说明哪些供应商是欧洲的）、&lt;strong&gt;审计报告&lt;/strong&gt;（说明哪些供应商没通过审计）、&lt;strong&gt;合同文档&lt;/strong&gt;（说明哪些供应商在处理 PII 数据）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但传统 RAG 干不了这件事&lt;/strong&gt;。为什么？&lt;/p&gt;
&lt;p&gt;因为传统 RAG 的检索逻辑是「找语义最相近的 Top-K 个文本块」。你问这个问题，它可能分别检索到：一些提到「欧洲供应商」的文本块、一些提到「安全审计」的文本块、一些提到「PII 处理」的文本块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但它没有任何机制把这三个集合做「交集」&lt;/strong&gt;。它只能把这些文本块一股脑扔给大模型，然后大模型自己也懵：我该怎么把这几份互相独立的资料关联起来？&lt;/p&gt;
&lt;p&gt;这种需要「A 关联到 B，B 关联到 C，然后从 A 推到 C」的问题，就叫&lt;strong&gt;多跳推理&lt;/strong&gt;（Multi-hop Reasoning）。传统 RAG 在这种场景下经常翻车，给出的答案要么不完整，要么干脆瞎编。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759062246-68ec5ed9-d798-4af2-adc0-1b64257738ad.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有一个经典的例子，问「爱因斯坦的导师的导师是谁？」。这个问题得先找到「爱因斯坦的导师是谁」，然后再找到「那个导师的导师是谁」。&lt;strong&gt;信息是分散在多个文档里的，传统 RAG 一次性检索根本搞不定这种链式推理&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;痛点二：全局性问题，它根本答不上&lt;/h3&gt;
&lt;p&gt;传统 RAG 的第二个硬伤，是&lt;strong&gt;答不了「全局性问题」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;什么叫全局性问题？我举几个例子你就有感觉了。&lt;/p&gt;
&lt;p&gt;比如你问「这本小说的主题思想是什么？」、「这份年度财报里，公司战略发生了哪些变化？」、「这个技术文档集合里，主要讨论的是哪几类方向？」这些问题有一个共同的特点：&lt;strong&gt;答案不在某一个文本块里，而是需要你通读所有文档、归纳总结才能得出来&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但传统 RAG 的本质是「检索」，它只会找 Top-K 个最像你问题的文本块。可你想问的是「主题思想」，哪个文本块会写「本书主题思想是 XXX」？几乎没有。所以它检索出来的 Top-K 个文本块，大概率只是一些零散的片段，大模型拿到这些片段根本没法总结出整体主题。&lt;/p&gt;
&lt;p&gt;这个问题在微软 GraphRAG 的论文里，被叫做 &lt;strong&gt;Query-Focused Summarization&lt;/strong&gt;（查询聚焦的摘要）问题，&lt;strong&gt;本质上不是一个「检索」任务，而是一个「全局归纳」任务&lt;/strong&gt;。传统 RAG 的 Top-K 检索机制，天生就处理不了这种需求。&lt;/p&gt;
&lt;p&gt;你想想，如果你在医院里问医生「过去一年咱们科室主要治疗的是哪几类病？」，医生要给出答案，得在脑子里把过去一年所有病例都过一遍、归归类。他不能只翻最上面的几份病例就告诉你答案吧？传统 RAG 干的就是这种「只翻最上面几份」的事情。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759147445-84018692-386f-4459-b818-899db4a3e915.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;痛点三：切块带来的「语义断裂」&lt;/h3&gt;
&lt;p&gt;传统 RAG 的第三个痛点，藏在我们经常忽略的细节里：&lt;strong&gt;文档切块（Chunking）本身就在毁坏语义&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你想啊，传统 RAG 第一步就是把文档切成 500 字或者 1000 字的文本块。这种切法看起来没问题，但实际上会带来三个麻烦：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一个麻烦，是「语义被切断了」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一段完整的论述可能横跨了三个段落，你按字符数切，就可能把一个完整的观点拦腰斩开。检索时只召回了上半段，下半段没召回，答案自然就不完整。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二个麻烦，是「实体之间的关系丢了」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举个医疗场景的例子：原文里写的是「卡托普利属于 ACEI 类药物，但严重肾功能不全者禁用」。切块之后，「卡托普利是 ACEI 药物」可能在一个块里，「严重肾功能不全者禁用」可能在另一个块里。&lt;/p&gt;
&lt;p&gt;这时候你问「高血压合并糖尿病患者首选哪几种 ACEI 药物？禁忌症是什么？」，传统 RAG 把这两个块都检索出来扔给大模型，大模型一看：诶，这里提到卡托普利，那里提到禁用……然后就可能给你来一句「卡托普利属于禁忌症药物」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你看，因果关系被切块这一步给毁了&lt;/strong&gt;。大模型拿到的是一堆「碎片化的事实」，但事实和事实之间原本的逻辑链条，没了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三个麻烦，是「检索噪声」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因为切块之后每个块都是独立的，语义相似的块不一定就是你真正想要的块。比如你问「苹果的营收」，可能会检索到一堆讨论「苹果」（水果）的文本块，因为它们在语义上都很接近「苹果」这个词。这种噪声进来之后，大模型就容易被误导。&lt;/p&gt;
&lt;h3&gt;一张表总结传统 RAG 的三大痛点&lt;/h3&gt;
&lt;p&gt;把上面讲的三大痛点汇总一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776764701869-166648b9-34de-431c-9050-11a5fabc144c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;痛点背后的根本原因：它不「理解关系」&lt;/h3&gt;
&lt;p&gt;你如果仔细琢磨上面这三个痛点，会发现它们背后其实是&lt;strong&gt;同一个根本问题&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统 RAG 做的是「找相似文本」，但企业真正需要的是「理解实体关系」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;向量检索这种方式，在「找长得像的句子」这件事上是很强的。但它没有机制去理解「这两个实体之间是什么关系」「这条信息和那条信息之间有没有因果链」。&lt;/p&gt;
&lt;p&gt;所以，如果你的业务场景只是「找到某条具体事实」（比如问「我们公司年假有多少天」），传统 RAG 完全够用。但只要问题一旦涉及&lt;strong&gt;关系、推理、全局归纳&lt;/strong&gt;，传统 RAG 就开始力不从心了。&lt;/p&gt;
&lt;p&gt;那问题来了，&lt;strong&gt;怎么让 RAG 真正「理解关系」呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案就是：&lt;strong&gt;引入知识图谱&lt;/strong&gt;。这也正是 GraphRAG 的核心思路。&lt;/p&gt;
&lt;p&gt;我们下一节就来聊聊，GraphRAG 到底是怎么把「找相似文本」升级成「理解关系」的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776758685570-621381a4-f5f3-4363-be77-39c43e636bf4.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2、什么是 GraphRAG？为什么会有 GraphRAG？&lt;/h2&gt;
&lt;h3&gt;先搞清楚 GraphRAG 到底是什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GraphRAG&lt;/strong&gt; 这个词，其实有两层含义。&lt;/p&gt;
&lt;p&gt;狭义上，它指的是&lt;strong&gt;微软研究院在 2024 年 4 月发布的那篇论文和配套的开源项目&lt;/strong&gt;。论文名字叫 &lt;strong&gt;《From Local to Global: A Graph RAG Approach to Query-Focused Summarization》&lt;/strong&gt;（从局部到全局：一种面向查询聚焦摘要的图 RAG 方法），作者是 Darren Edge、Ha Trinh 等人。&lt;/p&gt;
&lt;p&gt;广义上，它指的是&lt;strong&gt;所有基于知识图谱做 RAG 的方法&lt;/strong&gt;，业界还有很多变种（比如 NebulaGraph、HippoRAG、阿里云的 AnalyticDB GraphRAG 等）。&lt;/p&gt;
&lt;p&gt;咱们这篇文章&lt;strong&gt;主要讲的是微软这套 GraphRAG&lt;/strong&gt;，因为它目前影响力最大，开源社区也最活跃，几乎是所有 GraphRAG 方案的参照对象。后面如果没特别说明，我说的 GraphRAG 都指微软这一套。&lt;/p&gt;
&lt;p&gt;好，定义说清楚了，那 GraphRAG 到底在干嘛？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话概括：GraphRAG 就是用 LLM 把文档「读成一张知识图谱」，然后基于这张图谱来做检索和回答&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759298586-685b6f8f-9f8d-4c6d-8c3a-f09029ec7850.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;传统 RAG 和 GraphRAG 的核心差异是什么？&lt;/h3&gt;
&lt;p&gt;你如果把传统 RAG 和 GraphRAG 放一起对比，会发现它们的&lt;strong&gt;关键分歧就在于：知识是以什么形式存储的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;传统 RAG：知识以&lt;strong&gt;离散的文本块&lt;/strong&gt;形式存储。每个文本块都是独立的，块和块之间没有任何联系。&lt;/p&gt;
&lt;p&gt;GraphRAG：知识以&lt;strong&gt;图（Graph）&lt;/strong&gt; 的形式存储。文档里的人、地、物、概念（这些是「实体/节点」），以及它们之间的关系（这些是「边」），都被显式地提取出来，形成一张结构化的关系网络。&lt;/p&gt;
&lt;p&gt;我举个更具体的例子你就懂了。&lt;/p&gt;
&lt;p&gt;假如你有一段文本：「张三在 2025 年创立了 A 公司，李四是 A 公司的 CTO，A 公司主要做自动驾驶业务。」&lt;/p&gt;
&lt;p&gt;传统 RAG 会把这段话整体变成一个文本块，存到向量数据库里。你下次问「A 公司的 CTO 是谁？」，它靠语义相似度检索回这段话，然后让大模型去阅读理解，答出「李四」。&lt;/p&gt;
&lt;p&gt;GraphRAG 则会从这段话里&lt;strong&gt;抽取出实体和关系&lt;/strong&gt;，变成这样的结构化数据：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759424818-bcf1a8d2-db95-43a2-b84b-66b5358bf088.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你看出差别了吗？&lt;strong&gt;GraphRAG 里的知识不是「一段话」，而是一张「关系网」&lt;/strong&gt;。这张关系网里，每个节点是谁、每条边是什么关系，都是明确的、可追溯的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759496777-16ec8d47-5989-45bf-a648-e0959ddd5a94.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;为什么要搞成图？这么做到底好在哪？&lt;/h3&gt;
&lt;p&gt;你可能会问：费这么大劲把文档变成图，这么做到底有什么好处？&lt;/p&gt;
&lt;p&gt;其实就是&lt;strong&gt;为了解决咱们在第 1 章讲的那三个痛点&lt;/strong&gt;。咱们一个一个来看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759683279-196ae706-aa53-41fc-aa99-796953ee720f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一，多跳推理变得天然可行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;上一节咱们提到那个「欧洲供应商 + 审计没过 + 处理 PII 数据」的查询，传统 RAG 干不了，是因为它没法做「交集」。但在图上，这件事就变成了图遍历（Graph Traversal）：从「欧洲」这个节点出发，找它连着的所有「供应商」节点，再筛选出那些连着「审计未通过」的，再筛选出那些连着「PII 数据处理」的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图天然就擅长做这种「顺藤摸瓜」的事&lt;/strong&gt;。这也是 GraphRAG 在多跳问答任务上（比如 HotpotQA 数据集）比传统 RAG 的 F1 分数高 5% 以上的根本原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，全局性问题能答了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这是 GraphRAG 最核心的创新点，也是那篇论文标题里「From Local to Global」（从局部到全局）的来源。&lt;/p&gt;
&lt;p&gt;微软的做法很巧妙：&lt;strong&gt;先把整张知识图谱通过社区检测算法（比如 Leiden 算法）划分成若干个「社区」，再用 LLM 为每个社区生成一份摘要&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;什么叫社区？你可以把它理解成&lt;strong&gt;图里那些互相抱团、关系特别密切的一群节点&lt;/strong&gt;。比如在《三国演义》这本书里，刘关张就是一个小社区、曹操集团是一个社区、诸葛亮周边的人物又是一个社区。每个社区内部的人物关系非常紧密，但社区之间的连接就比较稀疏。&lt;/p&gt;
&lt;p&gt;一旦每个社区都有了摘要，你问「整本书主要讲了几路诸侯的斗争？」这种全局性问题时，GraphRAG 就可以&lt;strong&gt;把各个社区摘要拿出来归纳一下&lt;/strong&gt;，给你一个完整的答案。传统 RAG 没有这种「全局视角」，自然就答不出来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三，实体和关系显式化，切块语义断裂的问题也缓解了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因为 GraphRAG 已经提前把文档里的实体和关系抽出来了，哪怕原文里「卡托普利属于 ACEI」和「严重肾功能不全者禁用」分散在两个地方，它们在图上也能通过「卡托普利」这个节点连起来。检索时不会丢掉因果关系，大模型也就不容易混淆了。&lt;/p&gt;
&lt;h3&gt;一个更形象的类比：从「图书馆找书」到「带导游游书店」&lt;/h3&gt;
&lt;p&gt;讲到这儿，你可能已经有感觉了，但咱们再用一个生活化的类比来强化一下理解。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759779690-053098ba-b4f5-4296-bca6-ecb5c8c88f54.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统 RAG 像什么？像你去一个没有任何分类的图书馆找书&lt;/strong&gt;。所有书都按出版日期顺序摆在一起，你想找「和气候变化有关的书」，只能靠关键字搜索，一本一本翻，找到几本就交给你。你想问「这个图书馆最近这几年主要收藏的是哪几个方向的书？」对不起，没人能回答你。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GraphRAG 像什么？像你进了一个带导游的主题书店&lt;/strong&gt;。导游在你进门前已经把所有书按主题分好类了，每个主题区还贴了一张「这个主题区主要讨论什么」的概览海报。你要查具体的细节，导游带你精准定位；你要问宏观的主题，导游就把所有主题概览海报念给你听。&lt;/p&gt;
&lt;p&gt;你说这两种体验能一样吗？&lt;/p&gt;
&lt;h3&gt;GraphRAG 的诞生背景&lt;/h3&gt;
&lt;p&gt;最后聊聊 GraphRAG 为什么在 2024 年这个时间点被提出。&lt;/p&gt;
&lt;p&gt;其实「知识图谱」这个东西并不新鲜，早在 2012 年谷歌就提出过 Knowledge Graph 的概念。把知识图谱和 RAG 结合的想法也有人尝试过，但之前都不温不火，为什么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为之前构建知识图谱太痛苦了&lt;/strong&gt;。传统的知识图谱构建，要么靠人工标注（超级贵），要么靠专门训练的 NLP 模型（效果一般）。提取实体、关系、消歧、对齐……每一步都是技术壁垒。&lt;/p&gt;
&lt;p&gt;而大模型时代，这些事情&lt;strong&gt;全都可以交给 LLM 来做了&lt;/strong&gt;。你写几个提示词（Prompt），LLM 就能从一段文本里把实体、关系、属性都抽出来，效果还不错。这就让「从文档自动构建知识图谱」这件事从「需要专门团队做半年」降到了「写个 Pipeline 跑几个小时」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这也是 GraphRAG 能在 2024 年一下火起来的根本原因：不是思路新，而是 LLM 让这个思路变得可实现了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;好，到这儿你应该对 GraphRAG 是什么、为什么会有它，有了一个整体的认识。但你可能还有疑问：&lt;strong&gt;它具体是怎么工作的？实体怎么抽？社区怎么划？查询怎么做？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下一章，我们就来拆解 GraphRAG 的完整工作流程。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3、GraphRAG 的工作流程是怎样的？&lt;/h2&gt;
&lt;p&gt;搞清楚了 GraphRAG 是什么，咱们这一节就来拆解它&lt;strong&gt;到底是怎么工作的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;和传统 RAG 一样，GraphRAG 也分成两个大阶段：&lt;strong&gt;索引阶段（Indexing，离线）&lt;/strong&gt; 和 &lt;strong&gt;查询阶段（Query，在线）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759887116-0652b989-bbc5-4e46-8fd2-5c51069d6845.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;索引阶段做的事情&lt;/strong&gt;：把原始文档变成一张带有社区摘要的知识图谱。这个阶段很重，需要 LLM 大量调用，通常离线跑一次就行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;查询阶段做的事情&lt;/strong&gt;：用户提问时，基于这张图谱快速检索出相关信息，喂给 LLM 生成答案。&lt;/p&gt;
&lt;p&gt;咱们一个一个阶段来看。&lt;/p&gt;
&lt;h3&gt;索引阶段：5 步把文档变成知识图谱&lt;/h3&gt;
&lt;p&gt;微软 GraphRAG 的索引阶段，可以拆成 &lt;strong&gt;5 个核心步骤&lt;/strong&gt;。咱们用一个具体的例子来理解整个过程，比如说你有一本《三国演义》的电子书，要把它喂进 GraphRAG。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 1 步：文档切块（Text Chunking）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一步和传统 RAG 完全一样。把一整本书按一定的字符数（比如 1200 字）切成一个个文本块（GraphRAG 里叫 Text Unit）。&lt;/p&gt;
&lt;p&gt;为什么还要切块？因为 LLM 的上下文窗口有限，你没法把 50 万字的《三国演义》一次性塞给它。切成小块之后，后面的实体抽取就可以并发地对每个块单独处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 2 步：实体与关系提取（Entity &amp;amp; Relationship Extraction）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一步是 GraphRAG 的&lt;strong&gt;第一个核心创新点&lt;/strong&gt;。每个文本块都会单独发给 LLM，让它从里面把&lt;strong&gt;实体（Entity）&lt;/strong&gt; 和&lt;strong&gt;关系（Relationship）&lt;/strong&gt; 抽出来。&lt;/p&gt;
&lt;p&gt;GraphRAG 用的是一个专门设计的 Prompt，大致长这样（简化版）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你是一个实体关系抽取助手。请从下面的文本中：
1. 识别所有的实体，每个实体给出：
   - entity_name：实体名（大写）
   - entity_type：类型（比如 [人物, 组织, 地点, 事件]）
   - entity_description：实体的综合描述

2. 识别所有实体之间的关系，每对关系给出：
   - source_entity：源实体
   - target_entity：目标实体
   - relationship_description：关系的描述
   - relationship_strength：关系强度（1-10 的数字）

文本内容：
{input_text}

请按以下格式输出：
(&quot;entity&quot;|实体名|类型|描述)
(&quot;relationship&quot;|源|目标|描述|强度)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LLM 读完一段「刘备和关羽、张飞在桃园结义」的文本，会输出类似这样的结构化内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(&quot;entity&quot;|刘备|人物|蜀汉的建立者，三国时期重要人物)
(&quot;entity&quot;|关羽|人物|刘备的义弟，以忠义闻名)
(&quot;entity&quot;|张飞|人物|刘备的义弟，勇猛善战)
(&quot;relationship&quot;|刘备|关羽|桃园结义为兄弟，关系极为紧密|10)
(&quot;relationship&quot;|刘备|张飞|桃园结义为兄弟，关系极为紧密|10)
(&quot;relationship&quot;|关羽|张飞|桃园结义为兄弟|10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这一步跑完，所有文本块都会产出一堆实体节点和关系边&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但注意，这里会有一个麻烦：&lt;strong&gt;重复和歧义&lt;/strong&gt;。比如刘备可能在文本里被叫做「玄德」「先主」「刘皇叔」，LLM 抽取的时候可能把它们抽成了三个不同的实体。这个问题 GraphRAG 后面会通过「实体合并」和「社区检测」来一定程度上缓解，但不能完全解决。这也是我们第 4 章要讲的难点之一。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776759982921-b371f92b-5cf7-4c57-8aa9-604fbeb8145f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 3 步：生成实体/关系摘要（Summarization）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一步容易被忽略，但其实很关键。&lt;/p&gt;
&lt;p&gt;同一个实体（比如「刘备」）会在好多个文本块里被反复抽取，每次抽取的描述可能都不一样：「蜀汉的建立者」「关羽的义兄」「汉室宗亲」……&lt;/p&gt;
&lt;p&gt;GraphRAG 会把&lt;strong&gt;同一个实体在所有文本块里的描述汇总起来，让 LLM 合成一段更完整的实体描述&lt;/strong&gt;。关系也是同样的处理。&lt;/p&gt;
&lt;p&gt;这一步做完，每个实体节点就有了一段「综合描述」，这段描述会被向量化后存入向量数据库，供后面的 Local Search 用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 4 步：社区检测（Community Detection）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;到这一步，你手里已经有了一张完整的知识图谱，里面的节点是实体，边是关系。但这张图可能有成千上万个节点，怎么组织？&lt;/p&gt;
&lt;p&gt;GraphRAG 用的是&lt;strong&gt;Leiden 算法&lt;/strong&gt;（读作「莱顿」），这是一种&lt;strong&gt;层次化社区检测算法&lt;/strong&gt;，比常见的 Louvain 算法更稳定一些。&lt;/p&gt;
&lt;p&gt;Leiden 算法会把图谱划分成&lt;strong&gt;多个层级的社区&lt;/strong&gt;，这是它最有意思的地方。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760124608-b8f16385-f268-40c4-a20b-8e4197d0e55c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;咱们还是拿《三国演义》来说。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最粗的那一层（叫 Level 0）可能把整张图划成五到十个大社区，比如「曹魏集团」「蜀汉集团」「东吴集团」「黄巾起义相关」这种；&lt;/li&gt;
&lt;li&gt;再往下一层（Level 1），每个大社区又会被拆得更细，比如「蜀汉集团」里会分出「刘关张小团体」「诸葛亮周边」「五虎上将」等等；&lt;/li&gt;
&lt;li&gt;再继续往下 Level 2、Level 3，一路细分下去，直到社区没法再拆为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这种层次化的划分，就是 GraphRAG 后面能处理「不同粒度全局问题」的基础&lt;/strong&gt;。用户问得越宏观，就用越高层的社区；问得越具体，就用越低层的社区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 5 步：生成社区摘要（Community Reports）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;社区划分完之后，GraphRAG 会&lt;strong&gt;为每一个社区单独生成一份摘要报告&lt;/strong&gt;（叫 Community Report）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760215433-d1401df0-6650-431f-9d8d-bd3dba27437b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这一步也是靠 LLM 来做。GraphRAG 会把一个社区内部的所有实体、关系、还有可选的 claim 一股脑儿都喂给 LLM，让它写一份总结报告。这份报告里一般会写清楚这个社区讨论的主题是什么、里面最重要的实体和关系有哪些、有哪些关键发现，以及这个社区在整个数据集里的影响力评分。&lt;/p&gt;
&lt;p&gt;这些社区报告&lt;strong&gt;就是 GraphRAG 回答全局性问题的核心资产&lt;/strong&gt;。你可以把它们理解成图书馆里每个主题区前面贴的那张「本主题概览海报」。&lt;/p&gt;
&lt;p&gt;索引阶段到这里就结束了。&lt;strong&gt;可以看到，这个过程对 LLM 的调用量是非常大的&lt;/strong&gt;：每个文本块要调一次做实体抽取，每个实体/关系要调一次做摘要合成，每个社区要调一次做报告生成。一本 100 万 token 的书，索引一次可能要几千甚至上万次 LLM 调用，成本不便宜。这也是后面我们会讲到的痛点之一。&lt;/p&gt;
&lt;h3&gt;查询阶段：两种搜索模式各司其职&lt;/h3&gt;
&lt;p&gt;索引跑完之后，图谱就建好了，你有一整套实体节点 + 关系边 + 层次化社区 + 社区报告。&lt;strong&gt;真正用起来的时候，GraphRAG 提供了两种查询模式：Local Search 和 Global Search&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;（补充：后来 GraphRAG 官方还推出了第三种叫 DRIFT Search 的模式，相当于 Local 和 Global 的混合版，咱们不展开讲，感兴趣的同学可以看官方文档。）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模式一：Local Search（本地搜索）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：用户问的是关于&lt;strong&gt;某个具体实体&lt;/strong&gt;的问题。比如「诸葛亮都有哪些著名的计策？」「A 公司的 CTO 是谁？」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;定位入口实体&lt;/strong&gt;：先用问题的向量，在所有实体节点的向量索引里做相似度搜索，找到最相关的几个实体。比如问诸葛亮，就先定位到「诸葛亮」这个节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;顺藤摸瓜扩展&lt;/strong&gt;：从入口实体出发，沿着图的边扩展，找到相关的&lt;strong&gt;邻居实体、关系、原始文本块、所属社区报告&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;组装上下文&lt;/strong&gt;：把找到的这些信息整合成一段上下文（Context）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;交给 LLM 生成答案&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你看，&lt;strong&gt;Local Search 的核心是「从一个点扩散到它的邻居」&lt;/strong&gt;，很适合回答那些「围绕某个具体对象」的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760313736-6b737267-b1da-493d-a327-4222c1feaf97.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模式二：Global Search（全局搜索）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：用户问的是&lt;strong&gt;全局性、宏观性&lt;/strong&gt;的问题。比如「整本书讲了哪几派势力的斗争？」「这份报告的主要结论是什么？」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：GraphRAG 这里用的是一个&lt;strong&gt;Map-Reduce&lt;/strong&gt;的策略：
&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760414552-ce10675c-4f8c-47c2-b82f-371f6466dd90.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Map 阶段&lt;/strong&gt;：把某个层级（比如 Level 1）的所有社区报告都拿出来，分成好多批，对每一批都让 LLM 生成一个「中间答案」，并给每个观点打一个「重要性评分」&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce 阶段&lt;/strong&gt;：把所有中间答案里的观点汇总，按评分排序，保留最重要的那些，最后让 LLM 合并成一个完整的最终答案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个流程听起来有点绕，但它的本质是：&lt;strong&gt;把全局性问题拆分成多个社区内部的子问题并行解答，最后再汇总&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;打个比方，你问班长「咱们班这学期整体学习情况怎么样？」，班长不会去看每个同学的每张考卷，而是会先找每个小组的组长，让他们每人给一份自己组的总结，然后班长再把所有组的总结汇到一块儿给你看。GraphRAG 的 Global Search 干的就是这事儿。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么 Global Search 特别费钱？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你仔细想想就知道了：&lt;strong&gt;Map 阶段要遍历所有相关社区的报告&lt;/strong&gt;。如果层级选得低（比如 Level 2），可能涉及几百上千个社区，每个都要调一次 LLM 生成中间答案。一次全局查询可能就要消耗上万 token。&lt;/p&gt;
&lt;p&gt;这也是微软后来推出「动态社区选择」（Dynamic Community Selection）优化的原因：从根节点开始，只深入那些和问题相关的社区，跳过无关的子树，能大幅减少 LLM 调用次数。根据微软官方数据，这种优化能把平均参与计算的社区数从 1500 个降到 470 个左右。&lt;/p&gt;
&lt;h3&gt;一张图总结 GraphRAG 的完整工作流程&lt;/h3&gt;
&lt;p&gt;最后咱们把整个流程串起来：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760836761-e1a407ff-df74-4bd6-b4b1-eb01c2f34ae8.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;索引阶段（离线，一次性跑完）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760509060-f60311a7-308c-4f6b-988f-7f39d62a9513.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;查询阶段（在线，每次查询都跑）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760661776-4b9b6354-dbc1-43c9-aa26-9190291d6ea2.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;到这儿你应该已经把 GraphRAG 的整个流程搞清楚了。但你可能也注意到一个问题：&lt;strong&gt;这套方案虽然强大，但步骤多、调用贵、流程复杂&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;GraphRAG 在实际落地的时候，到底会撞上哪些坑？下一节咱们来盘一盘。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4、GraphRAG 的难点是什么？&lt;/h2&gt;
&lt;p&gt;读到这儿，你可能会觉得 GraphRAG 好像挺完美的，能解决传统 RAG 的痛点，又有清晰的流程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但这里我得给你泼一盆冷水&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;GraphRAG 虽然效果好，但它是一个「昂贵的妥协」。在真正的生产环境里，你会发现它有一堆让人头大的坑。&lt;strong&gt;这也是为什么很多团队兴冲冲地上 GraphRAG，试了几个月之后又默默退回到传统 RAG&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这一节咱们就把 GraphRAG 落地时常见的四大难点盘一盘。搞懂这些难点，你也就能理解为什么后来会出现 LightRAG 这种「轻量化改良方案」。&lt;/p&gt;
&lt;h3&gt;难点一：索引成本高得吓人（Token 焚烧炉）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GraphRAG 第一个也是最让人肉疼的难点，就是构建成本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;传统 RAG 的索引过程其实挺便宜的：切片 → 过一下 Embedding 模型 → 存进向量库。Embedding 模型的定价很低，每百万 token 也就几毛钱，整个过程几乎可以忽略成本。&lt;/p&gt;
&lt;p&gt;但 GraphRAG 的索引过程，&lt;strong&gt;简直就是一台 Token 焚烧炉&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776760979217-d5b3e1b9-5aab-44e2-abab-dbe000490e88.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你想想，它每一步几乎都在调 LLM。每个文本块要调 LLM 来抽实体和关系，抽完之后每个实体和关系还要再调 LLM 合成一段综合描述，然后每个社区又要调 LLM 生成一份社区报告，要是你还开了 claim 抽取，那又是一轮 LLM 调用来提取各种断言。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你把这些加起来就会发现，整个过程等于把所有原文通过 LLM 反复读了好几遍&lt;/strong&gt;，token 消耗轻轻松松就是原文的十倍甚至几十倍。&lt;/p&gt;
&lt;p&gt;具体花多少钱呢？有研究机构做过测算：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语料规模&lt;/th&gt;
&lt;th&gt;GraphRAG 索引成本（GPT-4o 定价）&lt;/th&gt;
&lt;th&gt;索引时间&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100 页（约 10 万 token）&lt;/td&gt;
&lt;td&gt;2-5&lt;/td&gt;
&lt;td&gt;10-30 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000 页（约 100 万 token）&lt;/td&gt;
&lt;td&gt;20-50&lt;/td&gt;
&lt;td&gt;1-3 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10000 页（约 1000 万 token）&lt;/td&gt;
&lt;td&gt;200-500&lt;/td&gt;
&lt;td&gt;5-15 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对比一下：同样 100 万 token，&lt;strong&gt;传统 RAG 的索引成本只要几美分，GraphRAG 要几十美元，差了三四个数量级&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这还不算最夸张的。有一份 arXiv 上的研究估算，&lt;strong&gt;给一份 5GB 的法律文档建 GraphRAG 索引，成本可能高达 3.3 万美元&lt;/strong&gt;。对于大部分团队来说，这个数字基本就意味着「不能玩」。&lt;/p&gt;
&lt;p&gt;这也是为什么微软自己在 2025 年推出了 &lt;strong&gt;LazyGraphRAG&lt;/strong&gt;，一个不在索引阶段做实体摘要和社区报告、改成查询时动态生成的版本，成本只有原版的 0.1%。&lt;/p&gt;
&lt;h3&gt;难点二：实体消歧，踩过的人都知道多痛&lt;/h3&gt;
&lt;p&gt;GraphRAG 第二个难点，也是&lt;strong&gt;很多人没踩坑之前意识不到的杀手级问题，就是实体消歧（Entity Disambiguation）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;什么叫实体消歧？我举几个例子你就明白了。&lt;/p&gt;
&lt;p&gt;你看「IBM」「国际商业机器」「IBM Corp.」「Big Blue」，这四个名字其实指的都是同一家公司，人一看就知道；但机器不知道。再比如「苹果」「苹果公司」「Apple」「Apple Inc.」，前三个在不同语境下既可能指水果也可能指公司，你得结合上下文去判断。还有一些场景更麻烦，比如「张三」这个名字，在几百篇文档里可能对应的根本就不是同一个人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而 LLM 做实体抽取，天然就是不一致的&lt;/strong&gt;。同一个实体在不同文档里被叫做不同名字，LLM 抽取出来之后，就变成了&lt;strong&gt;好几个独立的节点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这种情况下会发生什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一，你的知识图谱会被「近重复节点」塞满&lt;/strong&gt;。一张本该只有一万个节点的图，可能出现好几万个节点，其中一堆是同一个实体的不同化名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二，关系会碎片化&lt;/strong&gt;。假设有 100 份合同涉及 IBM，其中 60 份用「IBM」，40 份用「国际商业机器」。你查「IBM 的合同」，系统只会返回那 60 份。&lt;strong&gt;另外 40 份被挂在了另一个节点下，根本查不出来&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三，错误会指数级放大&lt;/strong&gt;。实体抽错一个，和它相连的所有关系都跟着错，下游社区检测也跟着错，最后生成的社区报告就跟原文对不上了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这个问题有多难？难到业界没有标准解法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;生产环境通常会叠加好几层策略：字符串编辑距离（处理拼写差异）、向量相似度（处理语义相似）、LLM 判别合并（处理复杂情况）、甚至还要人工兜底。但每一层都会&lt;strong&gt;增加索引成本和延迟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有位资深工程师说过一句话我觉得特别扎心：&lt;strong&gt;「实体碎片化的知识图谱，比没有知识图谱更糟糕，因为它制造了一种虚假的完整感」&lt;/strong&gt;。你以为图已经建好了，实际上里面全是断裂的关系，但你从外面根本看不出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776761072424-b5221e53-7c50-4634-ba3a-632d837e0c2f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;难点三：查询延迟，尤其是 Global Search&lt;/h3&gt;
&lt;p&gt;GraphRAG 的第三个难点，是&lt;strong&gt;查询延迟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;传统 RAG 的查询有多快？向量数据库的 ANN（近似最近邻）搜索通常&lt;strong&gt;毫秒级&lt;/strong&gt;返回，整个端到端的问答可能也就一两秒。&lt;/p&gt;
&lt;p&gt;GraphRAG 就完全不是一个级别了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local Search 还好&lt;/strong&gt;，定位入口实体 + 图遍历扩展 + 上下文组装 + LLM 生成，整体大概几秒钟，勉强能接受。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但 Global Search 就是个慢性子&lt;/strong&gt;。因为它本质是一个 Map-Reduce 过程：先是 Map 阶段，把某个层级下所有相关的社区报告拿出来，分批塞给 LLM 让它生成中间答案，要是层级选得低一点，可能涉及几百上千个社区，&lt;strong&gt;这就意味着要并行发起几百上千次 LLM 调用&lt;/strong&gt;；然后是 Reduce 阶段，再让 LLM 把所有中间答案汇总成一个最终回答。&lt;/p&gt;
&lt;p&gt;你这么一看就懂了，这种「先扇出再汇聚」的结构，&lt;strong&gt;一次 Global Search 的端到端延迟，经常在 10 秒到 1 分钟之间&lt;/strong&gt;。这种响应速度，做离线分析还行，做 C 端实时交互？完全不可能。&lt;/p&gt;
&lt;p&gt;你想啊，你问一个 ChatGPT，要等一分钟才出答案，谁能忍？&lt;/p&gt;
&lt;p&gt;微软后来也意识到这个问题，推出了&lt;strong&gt;动态社区选择&lt;/strong&gt;（Dynamic Community Selection）的优化，从根节点逐层下钻，只展开相关的社区，能把平均社区数从 1500 降到 470 左右。但再优化，Global Search 也还是比向量检索慢一个数量级。&lt;/p&gt;
&lt;h3&gt;难点四：增量更新，牵一发而动全身&lt;/h3&gt;
&lt;p&gt;这是 &lt;strong&gt;GraphRAG 最要命的一个难点&lt;/strong&gt;，我单独放在下一节展开讲。&lt;/p&gt;
&lt;p&gt;先给你一个直观感受：在传统 RAG 里，新增一份文档，就是把它切片、向量化、upsert 进向量库，完事。&lt;/p&gt;
&lt;p&gt;但在 GraphRAG 里，事情就没那么简单了。一份新文档进来，里面可能会冒出一个实体，它其实和图里已有的某个老节点是同一个东西，但名字不一样，于是实体消歧问题又来了。再比如新文档里抽出一堆关系，这些关系一加进图，原来的社区划分就可能过时了；社区一变，你之前花大价钱让 LLM 生成的那些社区摘要就全部跟着失效；等摘要重建完，整张图的向量索引还得跟着同步更新一遍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这种「数据互联性」带来的牵一发而动全身，让 GraphRAG 在动态场景下几乎无法高效维护&lt;/strong&gt;。很多团队的实际做法就是「定期全量重建」，但这又把成本问题放大了。&lt;/p&gt;
&lt;p&gt;增量更新的具体难在哪、业界有哪些应对方案，咱们下一章专门聊。&lt;/p&gt;
&lt;h3&gt;小结：GraphRAG 是「昂贵的妥协」&lt;/h3&gt;
&lt;p&gt;咱们把 GraphRAG 的四大难点汇总一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776761188829-88ee3787-a620-46bf-ac1d-dfe9b163ccdc.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;难点&lt;/th&gt;
&lt;th&gt;具体表现&lt;/th&gt;
&lt;th&gt;成本/影响&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;索引成本高&lt;/td&gt;
&lt;td&gt;Token 消耗是传统 RAG 的几十倍&lt;/td&gt;
&lt;td&gt;1M token 约 $20-50，大语料动辄几千几万美元&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实体消歧&lt;/td&gt;
&lt;td&gt;LLM 抽取不一致，同实体被拆成多个节点&lt;/td&gt;
&lt;td&gt;关系碎片化、检索召回不全&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询延迟&lt;/td&gt;
&lt;td&gt;Global Search 要遍历大量社区&lt;/td&gt;
&lt;td&gt;端到端延迟 10s-1min，C 端无法用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;增量更新&lt;/td&gt;
&lt;td&gt;数据互联性导致牵一发而动全身&lt;/td&gt;
&lt;td&gt;难以维护，很多团队只能定期重建&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;看到这儿你应该理解了，&lt;strong&gt;GraphRAG 不是银弹，它是一个「在特定场景下不得不接受的昂贵妥协」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果你的业务对成本敏感、对实时性要求高、数据还需要频繁更新，那原生的 GraphRAG 可能并不适合你。这也正是后面 LightRAG 这类「轻量化方案」的价值所在，它们在精度上做一些妥协，换来更低的成本和更好的增量支持。&lt;/p&gt;
&lt;p&gt;下一节，咱们就聚焦 GraphRAG 四大难点中最棘手的那个：&lt;strong&gt;增量更新&lt;/strong&gt;。看看业界是怎么应对的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5、GraphRAG 如何应对增量场景？&lt;/h2&gt;
&lt;p&gt;这一节咱们单独拎出来讲&lt;strong&gt;增量更新&lt;/strong&gt;，因为这是 GraphRAG 落地的时候最容易被低估、也是最让人头大的问题。&lt;/p&gt;
&lt;h3&gt;为什么增量更新对 GraphRAG 来说这么难？&lt;/h3&gt;
&lt;p&gt;在传统 RAG 里，你加一份新文档是什么流程？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;超简单&lt;/strong&gt;：把新文档切片 → 向量化 → upsert 到向量库，完事。整个过程几分钟搞定，几乎零副作用。&lt;/p&gt;
&lt;p&gt;在 GraphRAG 里，加一份新文档是什么流程？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完全不一样&lt;/strong&gt;。因为 GraphRAG 的知识是以图的形式组织的，&lt;strong&gt;数据和数据之间有复杂的连接关系&lt;/strong&gt;，一份新文档进来，可能会触发一串级联效应：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步的麻烦，新文档里的实体可能和现有图谱里的实体需要合并&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如老图谱里已经有了「IBM」这个节点，新文档里抽出来的叫「国际商业机器」。你需要识别出它们是同一个实体，然后合并。这就是&lt;strong&gt;增量版本的实体消歧问题&lt;/strong&gt;，不仅要做一次性消歧，还要在每次增量时都做一遍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步的麻烦，新的实体和关系会改变图的结构&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;图结构变了，意味着什么？&lt;strong&gt;原来的社区划分可能过时了&lt;/strong&gt;。可能新加入的一堆实体本应该属于某个新社区，或者老社区的边界发生了变化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步的麻烦，社区一变，下游的社区摘要就全失效了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你之前花了大价钱让 LLM 给每个社区生成的那些社区报告，现在可能和实际的图结构对不上了。要不要重新生成？重新生成就意味着&lt;strong&gt;又一次 Token 焚烧&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步的麻烦，向量索引也要同步更新&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;新的实体、关系、社区摘要都要重新向量化，存进向量库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你看，传统 RAG 里一步就能搞定的事情，在 GraphRAG 里变成了一连串「牵一发而动全身」的级联操作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776761278187-9f6d3f4c-a1c3-4289-8ddc-3a4a6115edb6.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有研究数据可以佐证这个难点：一项 2025 年的研究显示，在需要&lt;strong&gt;时间敏感&lt;/strong&gt;的查询上（也就是需要最新数据的场景），GraphRAG 的准确率比传统 RAG &lt;strong&gt;低了 16.6%&lt;/strong&gt;。根本原因就是因为 GraphRAG 更新成本太高，很多团队的图谱都是「过期的」。&lt;/p&gt;
&lt;h3&gt;业界应对增量问题的几种思路&lt;/h3&gt;
&lt;p&gt;这个问题这么难，业界有没有办法？有，但每种办法都是&lt;strong&gt;有代价的妥协&lt;/strong&gt;。下面几种主流思路，你可以根据自己的业务场景挑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路一：简单粗暴，定期全量重建&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最土但最省心的办法：每天、每周、或者每月定时把所有文档重新跑一遍 GraphRAG 索引。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：实现简单，一致性好，图谱永远是新鲜的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：贵。&lt;strong&gt;非常贵&lt;/strong&gt;。假设你有 1000 万 token 的语料，每天重建一次，一年就是 300 多次全量索引，光 LLM 调用费可能就要几万美元。而且重建过程中服务还要切换到「旧索引」上，运维复杂度也不低。&lt;/p&gt;
&lt;p&gt;适合什么场景？&lt;strong&gt;文档更新频率低 + 数据量不大 + 对实时性不敏感&lt;/strong&gt;的场景，比如公司年度法规库、历史档案库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路二：增量索引（Incremental Indexing）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;微软 GraphRAG 的后续版本（以及 Fast GraphRAG 这类改良方案）都支持了&lt;strong&gt;增量索引&lt;/strong&gt;，只处理新增的文档，把新抽出的实体和关系追加到已有图上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776761362958-ac6702f5-46fa-48e1-870b-2cc367835694.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心流程是这样的&lt;/strong&gt;：先对新文档做一次实体和关系抽取（注意，只处理新的，老文档完全不碰），然后把抽出来的新实体和图里的老实体做消歧合并（这一步也叫 upsert，实现上一般会用字符串编辑距离加上向量相似度组合判断），合并完之后把新关系追加到图上。这时候关键的一步来了：&lt;strong&gt;系统要判断哪些社区受到了这次更新的影响&lt;/strong&gt;（通常是通过图算法识别受影响的子图），然后&lt;strong&gt;只对这些受影响的社区重新做一次检测和摘要&lt;/strong&gt;，没受影响的部分保持原样不动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：不用全量重建，成本低很多。Fast GraphRAG 这种方案跑 430 个文档的增量，用本地 14B 模型大概 8.5 小时就能搞定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;strong&gt;精度会有损失&lt;/strong&gt;。因为社区划分本质是一个全局优化问题，你只重做局部社区，全局的社区划分就不一定是最优的。时间长了，图谱的质量会慢慢「漂移」，需要阶段性做一次全量重建来「校准」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路三：混合策略，小增量 + 周期性全量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是目前&lt;strong&gt;生产环境最常见的做法&lt;/strong&gt;，就是把前面两个思路结合起来用。日常有新文档进来，就跑一次增量索引，又快又便宜；但每隔一段时间（比如每个月一次），再做一次全量重建，把之前累积漂移掉的精度拉回来。&lt;/p&gt;
&lt;p&gt;这种方式的优点就是&lt;strong&gt;成本和质量的平衡最好&lt;/strong&gt;，缺点嘛，就是你得同时维护两套流程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路四：时间分区（Temporal Partitioning）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果你的数据有明显的时间属性（比如新闻、月度报告、季度财报），还有一种做法是&lt;strong&gt;按时间段分开建图&lt;/strong&gt;：2025 Q1 一张图，Q2 一张图，Q3 又是一张图。&lt;/p&gt;
&lt;p&gt;查询的时候，要么指定时间段查对应的图，要么跨图联邦查询再聚合。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：每张图相对小、更新隔离性好、老数据不受新数据影响。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：跨时间段的查询要专门做联邦策略，图和图之间的实体也需要做全局对齐。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路五：延迟实体合并 + 最终一致性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有些团队的做法更激进：&lt;strong&gt;索引时不做消歧&lt;/strong&gt;，只把所有实体直接加进去，哪怕同一个 IBM 有四个节点也无所谓。然后&lt;strong&gt;查询时再做合并&lt;/strong&gt;，或者&lt;strong&gt;异步离线跑消歧任务&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：索引极快，增量几乎零成本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：查询时可能要做更多的聚合和去重，查询延迟会变高。而且在没来得及异步合并之前，查询结果可能会漏召回。&lt;/p&gt;
&lt;h3&gt;GraphRAG 的「增量困境」其实推动了 LightRAG 的诞生&lt;/h3&gt;
&lt;p&gt;看到这儿你可能已经有感觉了：&lt;strong&gt;GraphRAG 的增量问题，本质上是「社区摘要」这个设计带来的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;之所以一动就牵一发而动全身，就是因为有「社区→社区摘要」这条依赖链。如果没有社区摘要，增量就只需要在图上加几个节点和边，就像传统 RAG 里 upsert 几个向量一样简单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那问题来了：能不能干脆不要社区摘要，用别的方式实现全局查询呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就是我们下一章要讲的 &lt;strong&gt;LightRAG&lt;/strong&gt; 给出的答案。&lt;/p&gt;
&lt;p&gt;LightRAG 的核心设计思想之一，就是「去社区化」，它根本不做 Leiden 社区检测和社区摘要，而是用另一套轻量级的「双层检索」机制来同时实现局部和全局查询。&lt;/p&gt;
&lt;p&gt;这么做下来的结果是什么呢？索引成本直接大幅降低，Token 消耗减少了 99%；增量更新也变得天然友好，新文档进来只需要追加节点和边就行，不用重新聚类；查询延迟也跟着降下来了。&lt;/p&gt;
&lt;p&gt;当然，这么做也是有代价的，代价是什么？下一章我们就来揭晓。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776761721925-973be0da-d2c1-454a-91bf-618f5739b39c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6、什么是 LightRAG？为什么会有 LightRAG？&lt;/h2&gt;
&lt;p&gt;讲完了 GraphRAG 的痛点和增量困境，咱们这一节就该聊聊业界给出的&lt;strong&gt;轻量化答案&lt;/strong&gt;：LightRAG。&lt;/p&gt;
&lt;h3&gt;LightRAG 的身世背景&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LightRAG 是由香港大学（HKU）数据智能实验室的团队在 2024 年 10 月发布的&lt;/strong&gt;，核心作者是 Zirui Guo、Lianghao Xia、Yanhua Yu、Tu Ao、Chao Huang 等人。&lt;/p&gt;
&lt;p&gt;论文标题叫 &lt;strong&gt;《LightRAG: Simple and Fast Retrieval-Augmented Generation》&lt;/strong&gt;（LightRAG：简洁快速的检索增强生成），名字里这两个词，「Simple」（简洁）和「Fast」（快），其实就是它设计哲学的概括。&lt;/p&gt;
&lt;p&gt;这篇论文发在 arXiv 上之后，反响非常热烈，后来在 &lt;strong&gt;EMNLP 2025（自然语言处理顶会）的 Findings Track 被接收&lt;/strong&gt;，还被称为是「&lt;strong&gt;GraphRAG 的高性价比改良版&lt;/strong&gt;」。项目开源在 GitHub 上（HKUDS/LightRAG），星星涨得飞快。&lt;/p&gt;
&lt;p&gt;你可以这么理解 LightRAG 的定位：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LightRAG 就是一款为了解决 GraphRAG 痛点而生的「轻量化 + 工程友好型」的 GraphRAG 替代方案&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它不追求「大而全」的复杂架构，而是在保留「图结构带来的关系理解能力」的前提下，把成本、速度、增量这三件事做到了极致。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776761840149-885382f9-1a1b-4dbe-870e-5d3f3d959914.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;为什么会有 LightRAG？直接原因就是 GraphRAG 的痛&lt;/h3&gt;
&lt;p&gt;这个问题其实我们前面几章已经铺垫得差不多了。LightRAG 作者在论文里也很直白地点出了他们做这个工作的动机，&lt;strong&gt;就是冲着 GraphRAG 的几个关键问题去的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;他们在论文里总结了两个关键局限。一个是&lt;strong&gt;现有基于图的 RAG 方法（主要就是指微软 GraphRAG）缺乏动态更新能力&lt;/strong&gt;，很难高效处理新信息的加入，数据一变整个图谱就得大改；另一个是&lt;strong&gt;全局查询的「暴力搜索」实在太低效&lt;/strong&gt;，GraphRAG 的 Global Search 是 Map-Reduce 遍历所有社区摘要的，在大规模语料上又慢又贵。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LightRAG 的目标，就是同时解决这两个问题&lt;/strong&gt;。而它解决问题的方式，是引入了三个核心创新。&lt;/p&gt;
&lt;h3&gt;LightRAG 的三个核心创新&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;创新一：图增强的文本索引（Graph-Enhanced Text Indexing）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LightRAG 和 GraphRAG 一样，也是用 LLM 从文档里抽实体和关系，构建知识图谱。但它做得&lt;strong&gt;更轻&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;轻在哪儿呢？&lt;strong&gt;它根本不做社区检测&lt;/strong&gt;，Leiden 算法那一套完全省掉了；&lt;strong&gt;也不做社区摘要&lt;/strong&gt;，那些最烧钱的 LLM 调用都不要了。它只保留&lt;strong&gt;实体节点 + 关系边&lt;/strong&gt;这个最核心的图结构，然后把实体和关系的描述都向量化，存进向量数据库。就这么简单。&lt;/p&gt;
&lt;p&gt;你可以把 LightRAG 的图理解成 GraphRAG 去掉「社区那层」的轻量版本：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GraphRAG&lt;/th&gt;
&lt;th&gt;LightRAG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;实体抽取&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关系抽取&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实体/关系摘要&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅（更轻量）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;社区检测（Leiden）&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;社区摘要&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;仅这两步省掉，就省了 80% 以上的 LLM 调用量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;创新二：双层检索范式（Dual-Level Retrieval）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是 LightRAG 最&lt;strong&gt;亮&lt;/strong&gt;的创新点。你可能会问：不做社区摘要了，那全局性问题怎么答？&lt;/p&gt;
&lt;p&gt;LightRAG 的做法是：&lt;strong&gt;不用提前做全局聚合，而是在查询时把查询本身拆成两层关键词，分别做检索&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举个例子你就明白了。如果你问「卡托普利的禁忌症是什么？」，低层关键词就是「卡托普利」「禁忌症」这种具体的实体和术语，它关注的是细节。而如果你问「国际贸易如何影响全球经济稳定？」，高层关键词则是「国际贸易」「全球经济稳定」「经济影响」这种抽象的主题和概念，它关注的是更宏观的东西。&lt;/p&gt;
&lt;p&gt;拿到这两组关键词之后，LightRAG 就兵分两路。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776761950199-fbfa2b81-6398-45ca-a851-c5e689fb70f1.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一路&lt;strong&gt;用低层关键词去向量库里找相关的实体&lt;/strong&gt;，另一路&lt;strong&gt;用高层关键词去向量库里找相关的关系&lt;/strong&gt;（也就是边的描述）。一个找「点」，一个找「线」，&lt;strong&gt;两种信息合起来组装成上下文&lt;/strong&gt;，最后让 LLM 生成答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这种双层检索的好处是&lt;/strong&gt;：不用预先生成一大堆社区摘要，每次查询只调用一次 LLM 做关键词抽取，然后几次向量检索就能搞定。相比 GraphRAG 的 Map-Reduce 式全局查询，&lt;strong&gt;LightRAG 的查询成本和延迟都降了一个数量级&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;创新三：增量更新算法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是 LightRAG 解决 GraphRAG 最痛那个点的方案。&lt;/p&gt;
&lt;p&gt;LightRAG 的增量更新，&lt;strong&gt;本质上就是「追加」两个字&lt;/strong&gt;。新文档来了，跑一下实体和关系抽取，然后把抽出来的新实体、新关系直接 upsert 到图和向量库里就行了。如果新实体和老实体有重合（比如都叫 IBM），那就合并节点的描述；要是完全是新实体，那就加个新节点。&lt;strong&gt;整个过程完全不需要重新做社区检测、不需要重新生成摘要、更不需要全量重建索引&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为它根本没有「社区」这一层，自然也就没有「社区失效」这种问题&lt;/strong&gt;。增量索引的流程变得跟传统 RAG 一样轻。&lt;/p&gt;
&lt;h3&gt;成本对比：LightRAG 有多便宜？&lt;/h3&gt;
&lt;p&gt;我们用几组真实数据看一下 LightRAG 到底便宜多少：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776762045703-09a3490a-791c-45f8-bdd8-1c744af65705.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;99% 的 Token 消耗降低、数量级的成本下降&lt;/strong&gt;，这就是 LightRAG 最大的吸引力。&lt;/p&gt;
&lt;h3&gt;LightRAG 的代价是什么？&lt;/h3&gt;
&lt;p&gt;当然，天下没有免费的午餐。LightRAG 牺牲了什么呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要是牺牲了「全局深度洞察」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;GraphRAG 的社区摘要虽然贵，但它提供的是一种&lt;strong&gt;经过 LLM 加工过的、具有层次抽象的全局知识视图&lt;/strong&gt;。对于那种需要深度宏观归纳的问题（比如「这本 500 页的小说整体想表达什么人生哲理？」），GraphRAG 的社区摘要能提供更好的全局洞察。&lt;/p&gt;
&lt;p&gt;LightRAG 的双层检索虽然轻量，但本质还是&lt;strong&gt;基于查询去「现场拼凑」&lt;/strong&gt; 全局视角，对极其复杂的全局归纳问题，深度可能不如 GraphRAG。&lt;/p&gt;
&lt;p&gt;所以&lt;strong&gt;两者的权衡其实很清楚&lt;/strong&gt;。如果你追求的是极致的全局深度洞察，而且预算管够，那 GraphRAG 是你的选择；反过来，如果你更在意的是成本可控、增量友好、实时性好，愿意在全局洞察深度上打个折扣，那 LightRAG 就更合适。&lt;/p&gt;
&lt;p&gt;好，到这儿你应该已经对 LightRAG 有了一个整体印象。但它的双层检索具体是怎么工作的？它的增量算法在实现层面是怎么做到「零额外成本」的？下一章咱们就来扒一扒 LightRAG 的完整工作流程。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7、LightRAG 的工作原理是怎样的？&lt;/h2&gt;
&lt;p&gt;这一章咱们来深入 LightRAG 的内部，看看它到底是怎么做到「既保留图的威力，又比 GraphRAG 便宜一个数量级」的。&lt;/p&gt;
&lt;p&gt;和 GraphRAG 一样，LightRAG 也分为&lt;strong&gt;索引阶段&lt;/strong&gt;（构建知识图谱）和&lt;strong&gt;查询阶段&lt;/strong&gt;（双层检索+生成）两大环节。但它的实现细节和 GraphRAG 有很大差异，咱们一步步拆。&lt;/p&gt;
&lt;h3&gt;索引阶段：轻量化的图构建&lt;/h3&gt;
&lt;p&gt;LightRAG 的索引阶段，&lt;strong&gt;只有三步&lt;/strong&gt;（对比 GraphRAG 的五步，少了两步最贵的）：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 1 步：文档切块（Chunking）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;和 GraphRAG 类似，LightRAG 也是先把文档切块，默认每块 1200 token，块和块之间有 100 token 的重叠（避免把一个完整语义单元切碎）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 2 步：实体和关系提取（Entity &amp;amp; Relationship Extraction）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LightRAG 会用 LLM 从每个文本块里抽实体和关系。每个实体会带上自己的名称、类型（比如是人物、组织还是地点）和一段综合描述；每条关系除了源实体、目标实体、关系描述之外，还会额外生成一个东西，叫「关系关键词」。&lt;/p&gt;
&lt;p&gt;这个「关系关键词」是 LightRAG 的一个小巧思，你可以先记着。它描述的是这条关系所代表的主题或概念，比如「张三创立 A 公司」这条关系，对应的关键词可能就是「创业、企业创立」。&lt;/p&gt;
&lt;p&gt;为什么要多生成一个关键词呢？这里先埋个伏笔，等到讲查询阶段的双层检索，你自然会明白这个关键词就是后面「高层检索」去命中相关关系的钥匙。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 3 步：键值对生成 + 去重（Key-Value Pair Generation &amp;amp; Deduplication）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一步是 LightRAG 和 GraphRAG 最大的差别所在。&lt;/p&gt;
&lt;p&gt;对于每个实体和关系，LightRAG 会生成一个&lt;strong&gt;键值对（Key-Value Pair）&lt;/strong&gt;。所谓 Key，就是一个便于检索匹配的短语，比如实体名或者关系关键词；而 Value 呢，就是一段详细的描述信息，留着后面组装上下文用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这种「键值对」的设计其实挺聪明的&lt;/strong&gt;。你想想查询的时候会发生什么？系统用查询关键词快速匹配 Key，一旦命中了，直接把对应的 Value 拿出来用作上下文，&lt;strong&gt;完全省掉了复杂的嵌入匹配和 chunk 遍历&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;去重也发生在这一步。&lt;strong&gt;同名的实体会被合并成一个节点，重复的关系会被合并成一条边&lt;/strong&gt;。比如「刘备」在 82 个文本块里被抽取了 82 次，就会被合并成一个节点，所有 82 次的描述会被聚合成一个更完整的实体描述。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意，LightRAG 的去重比较「朴素」&lt;/strong&gt;，主要基于名称匹配。它不会处理「张三」和「Zhang San」这种同人不同名的情况。论文作者在设计时也承认：这种轻量化的去重虽然会导致语义相似的实体被保留为多个节点（比如「AI」和「Artificial Intelligence」），但这不是大问题，因为&lt;strong&gt;查询时所有相似实体都会被一起检索出来，再通过多路召回合并&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776762116210-e975f618-dd00-4c23-9f6f-59e686179e7c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终的存储结构&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;索引跑完之后，LightRAG 的知识会以两种形式存下来。一份是&lt;strong&gt;知识图谱&lt;/strong&gt;，节点是实体、边是关系，默认用 NetworkX 作为存储后端（生产环境建议替换成 Neo4j 之类的图数据库）；另一份是&lt;strong&gt;向量数据库&lt;/strong&gt;，里面存着实体描述的向量、关系描述的向量，还有关系关键词的向量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你跟 GraphRAG 对比一下就能看出它的轻量化&lt;/strong&gt;：GraphRAG 要存原文文本块 + 实体 + 关系 + 社区 + 社区摘要，整整五套东西；而 LightRAG 只存实体 + 关系两套，就这么简单。&lt;/p&gt;
&lt;h3&gt;查询阶段：双层检索的精髓&lt;/h3&gt;
&lt;p&gt;索引搞定之后，查询阶段才是 LightRAG 真正的亮点。咱们一步步看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776765017347-e4d5d65d-a878-43fd-bccc-e31a0057a9cb.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 1 步：关键词抽取（Keyword Extraction）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用户的问题进来，LightRAG &lt;strong&gt;先用一次 LLM 调用&lt;/strong&gt;，从问题里抽出两种关键词。一种叫「低层关键词」（Low-level Keywords），它盯着的是具体的实体、细节和术语；另一种叫「高层关键词」（High-level Keywords），它关注的是更抽象的主题、总体概念。&lt;/p&gt;
&lt;p&gt;举个 LightRAG 论文里的原生例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;查询：&quot;国际贸易如何影响全球经济稳定？&quot;

LLM 输出：
{
  &quot;high_level_keywords&quot;: [&quot;国际贸易&quot;, &quot;全球经济稳定&quot;, &quot;经济影响&quot;],
  &quot;low_level_keywords&quot;: [&quot;贸易协定&quot;, &quot;关税&quot;, &quot;货币汇率&quot;, &quot;进口&quot;, &quot;出口&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你看，&lt;strong&gt;高层关键词是抽象主题，低层关键词是具体对象&lt;/strong&gt;。这个分层是整个双层检索的基础。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776762213807-f26f01d3-e037-4498-bb11-7e3809005f9b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 2 步：双层检索（Dual-Level Retrieval）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;拿到两组关键词之后，LightRAG 会分别做两路检索：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Low-level 检索&lt;/strong&gt;：用低层关键词去向量库里搜&lt;strong&gt;实体节点&lt;/strong&gt;。比如用「贸易协定」「关税」这些关键词，找到相关的实体节点（可能是某个具体的贸易协定、某种税种等）。然后从这些入口实体出发，在图上扩展到它们的&lt;strong&gt;一跳邻居&lt;/strong&gt;，收集相关的关系和描述。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;High-level 检索&lt;/strong&gt;：用高层关键词去向量库里搜&lt;strong&gt;关系边&lt;/strong&gt;。比如用「国际贸易」「经济影响」这些关键词，找到相关的关系边（可能是「贸易协定 → 影响 → 国家经济」这类关系）。然后从这些关系出发，聚合它们涉及的实体和更高层的语义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这两路检索是并行做的&lt;/strong&gt;，一路找「点」，一路找「线」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 3 步：上下文组装（Context Building）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把 Low-level 检索到的实体 + 邻居 + 相关文本块，和 High-level 检索到的关系 + 涉及实体 + 相关文本块&lt;strong&gt;拼在一起&lt;/strong&gt;，组装成一段完整的上下文。&lt;/p&gt;
&lt;p&gt;这里有个细节：LightRAG 会控制上下文的总 token 数（比如默认 4000 token），优先保留相关性最高的内容，避免上下文超过 LLM 窗口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 4 步：LLM 生成答案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把组装好的上下文和原始用户问题一起喂给 LLM，生成最终答案。&lt;/p&gt;
&lt;h3&gt;四种查询模式：灵活应对不同场景&lt;/h3&gt;
&lt;p&gt;除了上面描述的「双层检索」默认模式（也叫 Hybrid 模式），LightRAG 实际上提供了&lt;strong&gt;四种查询模式&lt;/strong&gt;，满足不同场景需求：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;用什么检索&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Naive（朴素）&lt;/td&gt;
&lt;td&gt;纯向量检索文本块（就是传统 RAG）&lt;/td&gt;
&lt;td&gt;简单事实问答，不需要关系推理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local（本地）&lt;/td&gt;
&lt;td&gt;只用 Low-level 检索（找具体实体）&lt;/td&gt;
&lt;td&gt;具体实体类问题，比如「这个 API 怎么用」&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global（全局）&lt;/td&gt;
&lt;td&gt;只用 High-level 检索（找关系主题）&lt;/td&gt;
&lt;td&gt;抽象概念、主题归纳类问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid（混合）&lt;/td&gt;
&lt;td&gt;Low-level + High-level 同时用&lt;/td&gt;
&lt;td&gt;复杂综合问题，默认推荐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;多数场景用 Hybrid 就够了，它是 LightRAG 推荐的默认模式&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;增量更新：真正的「轻量友好」&lt;/h3&gt;
&lt;p&gt;前面我们反复提到 LightRAG 的增量更新很好，具体好在哪？咱们看看它的增量算法做了什么：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776765122621-b1df1b16-b673-4dfb-aaae-5f7b561da4ba.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它的流程其实非常简单。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新文档来了，先切块，然后抽实体和关系，这一步只针对新文档，老文档完全不用再碰。&lt;/li&gt;
&lt;li&gt;接着就是&lt;strong&gt;增量合并&lt;/strong&gt;：如果抽出来的实体名字在已有图谱里能找到同名节点，就把新的描述追加或者合并到那个老节点上去；如果是全新的实体，那就添加一个新节点。关系的处理逻辑也一样，已有关系就合并描述，新关系就加条新边。&lt;/li&gt;
&lt;li&gt;最后一步，把新的实体、关系描述做一下向量化，追加到向量库里。就这样，整个增量过程就完成了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;整个过程里你会发现，GraphRAG 那些折磨人的步骤一个都不用做&lt;/strong&gt;。不用重新检测社区（根本没有社区这回事），不用重新生成社区摘要（压根就没摘要这一层），也不用重算全局图谱结构或者全量重建索引。&lt;/p&gt;
&lt;p&gt;这就是为什么 LightRAG 在增量场景下能做到「几乎零额外成本」。它的架构从设计之初就是为了增量友好来服务的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个细节值得提一下&lt;/strong&gt;：LightRAG 目前的增量处理对「语义冲突」并不敏感。比如 Day 1 抽到了「GPT-5.1 是 OpenAI 最新模型」，Day 2 又抽到「GPT-5.2 是 OpenAI 最新模型」，LightRAG 会把两条关系&lt;strong&gt;同时保留&lt;/strong&gt;，不会自动判定哪条更新。如果你的业务需要处理这种时效性冲突，得自己在上层加逻辑。&lt;/p&gt;
&lt;h3&gt;一张图总结 LightRAG 的完整工作原理&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776762795099-cd3d6f76-b414-403a-87de-61aa7e62cedc.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;索引阶段&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776762487496-40a57b8e-0483-4229-af1f-38117357af61.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;查询阶段（Hybrid 模式）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776762578083-12a9c7f7-2715-4933-820a-2a44f7264a2e.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;增量阶段&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776762670457-d9be8568-9ccd-4669-af2c-4479a6a02f62.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;到这儿你应该已经把 LightRAG 的工作原理搞清楚了。它整体比 GraphRAG 简洁得多，&lt;strong&gt;核心就是用「双层检索」替代了 GraphRAG 昂贵的「社区摘要」&lt;/strong&gt;，用「名称去重」替代了复杂的实体消歧。&lt;/p&gt;
&lt;p&gt;讲到这儿，两个技术的原理都讲完了，接下来咱们就该做一次完整的对比，看看这两者到底有哪些差异。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8、LightRAG 和 GraphRAG 有什么区别？&lt;/h2&gt;
&lt;p&gt;前面几章咱们分别拆解了 GraphRAG 和 LightRAG 的设计哲学、工作原理和核心创新。这一节，咱们来做一次&lt;strong&gt;系统性对比&lt;/strong&gt;，把两者的差异说透。&lt;/p&gt;
&lt;p&gt;我分几个维度一个一个看。&lt;/p&gt;
&lt;h3&gt;维度一：设计哲学上的分歧&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GraphRAG 的哲学是「预计算全局视角」&lt;/strong&gt;，在索引阶段就通过社区检测和社区摘要，把数据集的全局结构「嚼碎了」给你准备好。查询时直接拿现成的摘要用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LightRAG 的哲学是「按需检索，延迟聚合」&lt;/strong&gt;，索引阶段只做最核心的实体关系抽取，不做全局预计算。查询时通过双层关键词临时拼凑出局部和全局视角。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;打个比方&lt;/strong&gt;：GraphRAG 像一个很勤快的图书管理员，在你进图书馆之前已经把每个主题区都写好了概览海报。LightRAG 像一个聪明的助手，你告诉他你想了解什么，他&lt;strong&gt;现场&lt;/strong&gt;帮你把相关的书和书之间的关系梳理出来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;各有优劣&lt;/strong&gt;：前者查得快但维护贵，后者查得灵活但深度略浅。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776763214116-727929af-c6a0-44b3-81ea-1f7332864df9.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;维度二：索引阶段的差异&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;GraphRAG&lt;/th&gt;
&lt;th&gt;LightRAG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;文档切块&lt;/td&gt;
&lt;td&gt;✅ 一样&lt;/td&gt;
&lt;td&gt;✅ 一样&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM 抽实体关系&lt;/td&gt;
&lt;td&gt;✅ 一样&lt;/td&gt;
&lt;td&gt;✅ 一样&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实体/关系摘要&lt;/td&gt;
&lt;td&gt;✅ 每个实体/关系都要调 LLM 合成综合描述&lt;/td&gt;
&lt;td&gt;✅ 有，但更轻（直接 merge）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;社区检测（Leiden）&lt;/td&gt;
&lt;td&gt;✅ 必须步骤&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;完全没有&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;社区摘要生成&lt;/td&gt;
&lt;td&gt;✅ 每个社区调 LLM 生成一份详细报告&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;完全没有&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实体消歧&lt;/td&gt;
&lt;td&gt;需要复杂策略（字符串 + 向量 + LLM 判别）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;朴素名称匹配&lt;/strong&gt;，同名合并&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;索引阶段 LightRAG 省掉了两个最贵的环节&lt;/strong&gt;：社区检测和社区摘要生成。这也是为什么它的索引成本能做到 GraphRAG 的 1% 甚至更低。&lt;/p&gt;
&lt;h3&gt;维度三：查询阶段的差异&lt;/h3&gt;
&lt;p&gt;先看 GraphRAG 的查询。如果你用的是 Local Search，它会先用向量找到入口实体，然后沿着图遍历扩展，最后组装上下文；如果是 Global Search，则是 Map-Reduce 式地遍历&lt;strong&gt;所有&lt;/strong&gt;相关社区的摘要，每个社区生成中间答案，再汇总。&lt;/p&gt;
&lt;p&gt;再看 LightRAG 的查询（Hybrid 模式）。一上来先用一次 LLM 把问题拆成双层关键词；低层关键词拿去检索实体，扩展到一跳邻居；高层关键词拿去检索关系，聚合涉及的实体；两路最后合并，交给 LLM 生成答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最大的差别在哪呢？&lt;/strong&gt; GraphRAG 的 Global Search 要&lt;strong&gt;遍历几百上千个社区&lt;/strong&gt;，而 LightRAG 的 Hybrid 只做&lt;strong&gt;两路并行的向量检索&lt;/strong&gt;。这就是为什么 LightRAG 的查询延迟能低一个数量级。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776763304062-27e86bff-5726-4535-9cfa-1f669ec8236b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;维度四：增量更新能力&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;GraphRAG&lt;/th&gt;
&lt;th&gt;LightRAG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;新增一份文档&lt;/td&gt;
&lt;td&gt;可能触发社区重划、摘要重建、向量更新（级联复杂）&lt;/td&gt;
&lt;td&gt;只需抽实体关系 + 名称合并 + 向量追加&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除一份文档&lt;/td&gt;
&lt;td&gt;同样会触发级联影响&lt;/td&gt;
&lt;td&gt;删除对应节点和边即可&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大批量更新&lt;/td&gt;
&lt;td&gt;通常需要定期全量重建&lt;/td&gt;
&lt;td&gt;直接增量，不用重建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时效性冲突处理&lt;/td&gt;
&lt;td&gt;依赖复杂的版本化和重建策略&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;目前能力较弱&lt;/strong&gt;，会同时保留冲突信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;LightRAG 在增量场景下的优势非常明显&lt;/strong&gt;，这也是它相比 GraphRAG 最突出的工程价值。&lt;/p&gt;
&lt;p&gt;但要注意，&lt;strong&gt;时效性冲突处理这一点 LightRAG 反而是短板&lt;/strong&gt;，如果你的业务场景里有很多「过时被替代」的信息（比如新版产品替换老版），得在上层加逻辑。&lt;/p&gt;
&lt;h3&gt;维度五：成本对比（真刀真枪的数字）&lt;/h3&gt;
&lt;p&gt;这里给一组&lt;strong&gt;相对权威&lt;/strong&gt;的对比数据（来源：LightRAG 论文、社区基准测试）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;GraphRAG&lt;/th&gt;
&lt;th&gt;LightRAG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;索引 100 万 token 的成本&lt;/td&gt;
&lt;td&gt;4-7美元(原版)到20-50(贵模型)&lt;/td&gt;
&lt;td&gt;约 $0.15 美元&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单次查询 Token 消耗&lt;/td&gt;
&lt;td&gt;约 13,000&lt;/td&gt;
&lt;td&gt;约 100-1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单次查询 API 调用&lt;/td&gt;
&lt;td&gt;几百次（Global Search 特别多）&lt;/td&gt;
&lt;td&gt;几次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单次查询延迟&lt;/td&gt;
&lt;td&gt;10 秒-1 分钟（Global）&lt;/td&gt;
&lt;td&gt;1-3 秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;增量一次更新&lt;/td&gt;
&lt;td&gt;成本高（可能触发社区重建）&lt;/td&gt;
&lt;td&gt;几乎零额外成本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Token 消耗降低 99%&lt;/strong&gt;，这是 LightRAG 论文里最吸引人的一行字，也是落到钱包上的实实在在的差距。&lt;/p&gt;
&lt;h3&gt;维度六：精度表现（效果谁更好？）&lt;/h3&gt;
&lt;p&gt;很多人会问：LightRAG 这么便宜，精度是不是不行？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并不是&lt;/strong&gt;。根据 LightRAG 论文和社区的多项基准测试，&lt;strong&gt;LightRAG 在四个主流数据集（Agriculture、Computer Science、Legal、Mix）上的四个评测维度（全面性、多样性、赋能性、整体）上，全部击败了 NaiveRAG、RQ-RAG、HyDE 甚至是微软的 GraphRAG&lt;/strong&gt;。尤其是在百万 token 级的大规模语料上，LightRAG 的优势更明显；在「多样性」这个维度上，LightRAG 更是大幅领先，这个得益于双层检索带来的信息广度。&lt;/p&gt;
&lt;p&gt;但咱们也得客观看问题。在&lt;strong&gt;需要极深度全局归纳&lt;/strong&gt;的场景下，比如「这本书的终极主题思想是什么」这种问题，GraphRAG 的社区摘要仍然是有优势的；在&lt;strong&gt;实体消歧要求严格&lt;/strong&gt;的场景下（像金融合同、医疗诊断这类容错率极低的业务），GraphRAG 那套复杂的消歧策略也更靠谱。&lt;/p&gt;
&lt;h3&gt;一张大表总结所有差异&lt;/h3&gt;
&lt;p&gt;最后，咱们用一张表把所有维度的对比汇总起来，方便你收藏：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比维度&lt;/th&gt;
&lt;th&gt;GraphRAG（微软）&lt;/th&gt;
&lt;th&gt;LightRAG（港大）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;发布时间&lt;/td&gt;
&lt;td&gt;2024 年 4 月&lt;/td&gt;
&lt;td&gt;2024 年 10 月&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;核心创新&lt;/td&gt;
&lt;td&gt;社区检测 + 社区摘要&lt;/td&gt;
&lt;td&gt;双层检索 + 增量友好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引步骤&lt;/td&gt;
&lt;td&gt;5 步（含社区检测和摘要）&lt;/td&gt;
&lt;td&gt;3 步（无社区）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询方式&lt;/td&gt;
&lt;td&gt;Local / Global / DRIFT&lt;/td&gt;
&lt;td&gt;Naive / Local / Global / Hybrid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全局查询机制&lt;/td&gt;
&lt;td&gt;Map-Reduce 遍历社区摘要&lt;/td&gt;
&lt;td&gt;高层关键词检索关系边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引成本&lt;/td&gt;
&lt;td&gt;高（Token 焚烧炉）&lt;/td&gt;
&lt;td&gt;低（减少 99%）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询延迟&lt;/td&gt;
&lt;td&gt;慢（Global 可达分钟级）&lt;/td&gt;
&lt;td&gt;快（秒级）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;增量更新&lt;/td&gt;
&lt;td&gt;难（级联复杂）&lt;/td&gt;
&lt;td&gt;易（直接追加）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实体消歧&lt;/td&gt;
&lt;td&gt;多层策略，精度高但贵&lt;/td&gt;
&lt;td&gt;朴素名称匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;深度全局洞察&lt;/td&gt;
&lt;td&gt;强（社区摘要）&lt;/td&gt;
&lt;td&gt;一般（临场组装）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工程复杂度&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开源协议&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Stars&lt;/td&gt;
&lt;td&gt;20k+&lt;/td&gt;
&lt;td&gt;10k+（增长飞快）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适合业务&lt;/td&gt;
&lt;td&gt;深度分析、高精度、成本不敏感&lt;/td&gt;
&lt;td&gt;实时更新、成本敏感、轻量部署&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776763463887-e8aadfd5-ef24-4c56-a6fa-7ac6fd7524d7.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看完这张对比表，你应该已经对两者的差异心里有数了。但光知道差异还不够，&lt;strong&gt;真正关键的是：什么时候该选哪个？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接下来两节，咱们就专门聊 GraphRAG 和 LightRAG 各自的适用场景。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9、什么场景下需要 GraphRAG？&lt;/h2&gt;
&lt;p&gt;搞清楚了差异，咱们来聊聊&lt;strong&gt;哪些场景真的值得上 GraphRAG&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个问题很重要，因为 GraphRAG 的成本摆在那儿，如果你的业务场景用传统 RAG 或 LightRAG 就能搞定，那花几千几万美元上 GraphRAG 就是纯浪费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么场景真的值得？&lt;/strong&gt; 我总结下来大概有这么几类。&lt;/p&gt;
&lt;h3&gt;场景一：需要「全局洞察」的深度内容分析&lt;/h3&gt;
&lt;p&gt;这是 GraphRAG 最核心的主场。典型场景包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;长篇文学作品的主题分析&lt;/strong&gt;。比如你要让 AI 帮你分析一本 50 万字的小说整体想表达什么、各个角色之间的关系网络、不同社会阶层的冲突模式。这种问题没有任何一个文本块能给你答案，必须要通读全文、做全局归纳。GraphRAG 的社区摘要设计就是为这种场景量身定制的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多篇研究报告的综述&lt;/strong&gt;。咨询公司经常要做这种事：「过去十年这个行业的主要发展脉络是什么？」「这 50 份白皮书里主流观点的演进趋势是什么？」传统 RAG 会给你一堆零散文本片段，GraphRAG 则能通过社区报告给你提炼出主题层面的宏观答案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大规模专利 / 学术论文分析&lt;/strong&gt;。比如医药公司想知道「过去五年某个疾病领域的研究热点是怎么演进的」，这需要在数万篇论文上做主题发现和演进追踪，GraphRAG 的层次化社区结构能很好地支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776763560718-0aab0e69-1f83-4daf-bc8a-ed375e24d721.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;场景二：跨文档多跳推理的高价值场景&lt;/h3&gt;
&lt;p&gt;这类场景的共同点是：&lt;strong&gt;回答一个问题需要跨多个文档做关系推理，而且答错了代价很大&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;合规与审计追踪&lt;/strong&gt;。比如银行要回答：「哪些贷款客户关联的公司最近被监管点名，而且在我们这里的信贷额度超过 5000 万？」这个问题需要打通「客户-公司关联-监管记录-信贷档案」四类数据的关系。GraphRAG 的图遍历能把这条链路完整走通。根据微软官方基准测试，GraphRAG 在这类「关系遍历型」查询上的&lt;strong&gt;精度比传统 RAG 提升 40%-60%&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;医疗诊断辅助&lt;/strong&gt;。比如医生问：「糖尿病患者合并高血压，使用 ACEI 类药物的禁忌症有哪些？」这需要把「疾病-药物-禁忌症」的关系准确追溯。GraphRAG 能通过知识图谱的显式关系避免传统 RAG 常犯的「因果关系搞错」的错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;法律案例检索&lt;/strong&gt;。律师需要找到「过去类似的合同争议在不同司法管辖区是如何被判决的」，这种问题需要关联「案件-判决-法规-判例引用」多层关系。GraphRAG 能通过图结构显式保留这些引用和判决链。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金融研报深度分析&lt;/strong&gt;。分析师问：「A 公司的哪些高管最近有变动，其中有没有出现在竞争对手公司的前任高管名单里？」这种跨实体、跨时间、跨文档的关联推理，只有 GraphRAG 能干得好。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;场景三：数据相对静态、更新不频繁的知识库&lt;/h3&gt;
&lt;p&gt;因为 GraphRAG 的增量更新成本高，它最适合那些&lt;strong&gt;建好之后长期服务&lt;/strong&gt;的静态知识库。像企业内部的规章制度库、历史档案、法律条文库、已经归档的项目资料、公司的年度研究报告集，这类东西几个月甚至一年才更新一次。&lt;/p&gt;
&lt;p&gt;你想想，如果你的数据一年只更新一两次，那花一笔钱建好索引，之后的查询收益是长期的，这笔成本就能慢慢摊薄。&lt;/p&gt;
&lt;h3&gt;场景四：对精度和可解释性要求极高的场景&lt;/h3&gt;
&lt;p&gt;GraphRAG 还有一个容易被忽略的优势：&lt;strong&gt;可解释性强&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因为它的知识是显式的图结构，每个答案都能追溯到具体的实体、关系和原始文本块。这对于&lt;strong&gt;需要审计追踪的场景&lt;/strong&gt;非常关键。&lt;/p&gt;
&lt;p&gt;比如金融合规，每个决策都必须有据可查；医疗诊断，每个回答都要有明确的医学证据支撑；法律咨询里，每条建议都要能对应到具体的法条或判例；政府政务更不用说，每个答案都必须能追溯回具体的政策文件。&lt;/p&gt;
&lt;p&gt;你想啊，如果你是 AI 系统回答用户的合规问题，出了事情没法追溯到具体的证据链，那这个系统在金融监管层面根本不敢上线。GraphRAG 的「节点-边-社区」结构天然就是可审计的。&lt;/p&gt;
&lt;h3&gt;什么场景下不要上 GraphRAG？&lt;/h3&gt;
&lt;p&gt;反过来，哪些场景千万不要为了「赶时髦」上 GraphRAG 呢？&lt;/p&gt;
&lt;p&gt;数据量如果很小，比如不到 10 万 token，就没必要折腾；业务变化快、数据每天都在更新的场景，维护成本会把你搞崩溃；预算紧张的初创公司和个人项目，索引那笔钱根本不划算；做 C 端实时交互的产品，Global Search 那个延迟用户根本忍不了；以及那些单跳简单事实问答就能搞定的业务，比如客服 FAQ、产品说明书问答，上 GraphRAG 就是大炮打蚊子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话总结&lt;/strong&gt;：GraphRAG 是「&lt;strong&gt;深度分析场景的重型武器&lt;/strong&gt;」，它贵、慢、复杂，但在关键场景能给你带来其他方案给不了的深度和精度。如果你的业务就是想做「浅浅的客服问答」，那就别折腾它了。&lt;/p&gt;
&lt;p&gt;那么&lt;strong&gt;LightRAG 适合的场景&lt;/strong&gt;又是什么呢？下一节继续聊。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10、什么场景下需要 LightRAG？&lt;/h2&gt;
&lt;p&gt;讲完 GraphRAG 的场景，咱们来聊 LightRAG。LightRAG 的定位其实可以用一句话概括：&lt;/p&gt;
&lt;p&gt;它适合那些「想吃图 RAG 的红利，但又扛不住 GraphRAG 的成本和维护负担」的场景。&lt;/p&gt;
&lt;p&gt;具体有哪些？咱们分类看。&lt;/p&gt;
&lt;h3&gt;场景一：数据频繁更新的业务&lt;/h3&gt;
&lt;p&gt;这是 LightRAG 最核心的主场：&lt;strong&gt;凡是需要数据持续增量更新的业务，选它就对了&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;企业知识库的日常维护&lt;/strong&gt;。很多公司内部的知识库每天都有新的文档进来：产品发布说明、客户反馈、项目总结、技术方案……如果用 GraphRAG，每加一份文档都可能要重算社区、重建摘要。用 LightRAG，新文档来了就直接 insert，几乎零维护成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新闻聚合和实时资讯&lt;/strong&gt;。比如你做一个金融新闻分析系统，每天有几千条新闻进来。GraphRAG 根本跟不上更新节奏，LightRAG 则可以做到「新闻进来几分钟后就能被查询」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户支持系统&lt;/strong&gt;。CRM 系统每天都有新的工单、投诉、解决方案记录。LightRAG 的增量友好特性可以让这类动态知识库长期保持新鲜。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持续更新的技术文档&lt;/strong&gt;。比如 GitHub 开源项目的文档、API 文档、内部技术 wiki，这些东西几乎每天都在变。LightRAG 的「追加式增量」设计就是为这种场景量身定制的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776763714574-59a66f9a-cb8e-4f17-82ba-9d3f71474b20.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;场景二：成本敏感的中小团队和初创公司&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一句话：预算紧张、但又想做出效果好的 RAG 应用，选 LightRAG&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;创业公司或者小团队做 AI 应用，预算都比较紧。你一个 MVP 都没跑起来呢，花几千美元建 GraphRAG 索引就说不过去了。LightRAG 把索引成本降低到几美分量级，个人开发者都能玩得起。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;个人知识管理&lt;/strong&gt;。有些技术爱好者想把自己的读书笔记、博客收藏、会议记录都做成一个 AI 助手，查询时能引经据典。GraphRAG 成本太高，LightRAG 很合适。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;教育和学习场景&lt;/strong&gt;。老师想把几百份讲义、习题、参考资料做成一个 AI 问答助手给学生用。LightRAG 可以用开源模型本地跑（比如 Ollama + Qwen），完全零 API 成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SaaS 产品的知识库功能&lt;/strong&gt;。如果你做 SaaS，想给每个客户都开一个独立的知识库，GraphRAG 的成本会随着客户数线性放大。LightRAG 的低成本让多租户知识库变得可行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;场景三：实时性要求高的 C 端应用&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LightRAG 的查询延迟是秒级的，能支持 C 端实时交互&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客服聊天机器人&lt;/strong&gt;。用户问问题得立刻给响应，等 GraphRAG 的 Global Search 跑 30 秒谁忍得了？LightRAG 的 Hybrid 查询通常 1-3 秒就能出结果，体验好很多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能客户助手 / AI 助理&lt;/strong&gt;。比如你做一个编程助手，用户问「我上次写的那个项目用了什么数据库？」，你希望它能立刻从项目历史记录里找到答案。LightRAG 的双层检索在秒级返回，不会拖累交互节奏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音交互场景&lt;/strong&gt;。对于带语音的 AI 应用，延迟超过 3 秒用户就会觉得「AI 卡了」。GraphRAG 的 Global Search 完全不满足这个要求，LightRAG 则可以很好适配。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;场景四：轻量部署、本地化环境&lt;/h3&gt;
&lt;p&gt;LightRAG 有个很实用的优势：&lt;strong&gt;它可以在普通 CPU 机器上跑&lt;/strong&gt;，不强依赖 GPU。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;边缘设备部署&lt;/strong&gt;。比如把 RAG 系统部署到工厂车间的边缘盒子里，帮工人查技术手册。LightRAG 的低资源消耗让这种部署成为可能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有化内网部署&lt;/strong&gt;。一些对数据隐私要求高的行业（比如金融、医疗、政府），不能用公网 LLM 服务。LightRAG + 本地开源大模型（如 Qwen2.5、GLM）可以完全离线运行，数据不出内网。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地个人 AI 助手&lt;/strong&gt;。用 Ollama 跑本地大模型，配合 LightRAG，普通笔记本就能跑起来一个可用的知识库问答系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;场景五：中小规模知识库&lt;/h3&gt;
&lt;p&gt;从数据规模看：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语料规模&lt;/th&gt;
&lt;th&gt;推荐方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;少于 10 万 token&lt;/td&gt;
&lt;td&gt;传统 RAG（没必要上图）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 万 - 500 万 token&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;LightRAG 最佳&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 万 - 5000 万 token&lt;/td&gt;
&lt;td&gt;LightRAG 或 GraphRAG 都可以，看业务侧重&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大于 5000 万 token&lt;/td&gt;
&lt;td&gt;GraphRAG 更合适（规模越大，社区摘要的价值越大）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;中小规模是 LightRAG 的甜蜜区&lt;/strong&gt;。大多数企业的内部知识库其实都落在这个区间。&lt;/p&gt;
&lt;h3&gt;混合架构：两个都要，分而治之&lt;/h3&gt;
&lt;p&gt;还有一种越来越常见的做法，那就是&lt;strong&gt;混合架构&lt;/strong&gt;。具体怎么搭呢？&lt;/p&gt;
&lt;p&gt;对于那些稳定的、历史性的核心知识（比如公司规章、法规库），用 GraphRAG 建一次索引，做深度分析；对于动态的、日常变化的增量数据（比如工单、新闻、产品更新），就用 LightRAG 来做实时响应。然后在查询时再搭一个&lt;strong&gt;路由器（Query Router）&lt;/strong&gt;，根据查询的类型，把它分流到对应的系统里去。&lt;/p&gt;
&lt;p&gt;这种混合架构的好处是能同时享受两者的优势，代价嘛，就是工程复杂度会上升。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776763777022-f8e8be3b-3888-4514-9b7c-9eab6a91951c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;一句话总结&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GraphRAG 是深度分析的重型武器，LightRAG 是日常使用的轻巧刀具&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;大多数企业的 RAG 落地其实用 LightRAG 就能覆盖 80% 的需求，剩下 20% 的深度分析需求再上 GraphRAG 做补充，这可能是目前最务实的选型策略。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;咱们花了很长的篇幅，把 GraphRAG 和 LightRAG 这两个技术从痛点、原理、难点到场景都讲了一遍。最后用一张图把整个脉络串起来：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RAG 技术的演进线&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776764505226-61ce4c12-532d-40c8-b34a-61a36c33e883.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一张决策树帮你选型&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776764589043-738a191b-a353-43f0-bb1f-e2beab4b7ab5.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后给一个务实的建议&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;不要一上来就选 GraphRAG 这种「重型方案」。&lt;strong&gt;先用传统 RAG 搭个 MVP，跑一段时间看看用户到底在问什么类型的问题&lt;/strong&gt;。如果发现大量问题涉及跨文档关系、全局主题，再考虑升级到 LightRAG。如果 LightRAG 也搞不定你那部分高精度、深度分析的需求，才需要上 GraphRAG。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RAG 技术的发展，本质上是在「精度」「成本」「速度」「维护」之间不断做权衡&lt;/strong&gt;。没有银弹，只有最适合你业务场景的方案。&lt;/p&gt;
&lt;p&gt;希望这篇文章能帮你理清 GraphRAG 和 LightRAG 到底是什么、怎么选。&lt;/p&gt;
&lt;p&gt;我们下次见！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/rag/graphrag-lightrag.html&quot;&gt;agent/rag/graphrag-lightrag.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🦺Harness Engineering 是什么？AI Agent 越用越聪明的秘密</title><link>https://www.corgi37.top/posts/harness-engineering/</link><guid isPermaLink="true">https://www.corgi37.top/posts/harness-engineering/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;不知道你最近是不是和我一样有这种感觉：AI 圈造新词的速度是真的快。&lt;/p&gt;
&lt;p&gt;Prompt Engineering 这个词大家还没整明白呢，Context Engineering 又火了一年，资料还在到处转。&lt;/p&gt;
&lt;p&gt;最近圈子里又冒出来一个新词，叫「&lt;strong&gt;Harness Engineering&lt;/strong&gt;」，翻译过来叫「驾驭工程」。&lt;/p&gt;
&lt;p&gt;三天一概念，五天一炸裂，一周一个新名词。&lt;/p&gt;
&lt;p&gt;你是不是已经麻了？&lt;/p&gt;
&lt;p&gt;但麻归麻，我还是去把这个新词的来龙去脉认认真真扒了一遍。&lt;/p&gt;
&lt;p&gt;扒完之后我发现，这次还真不是又一个换皮的概念，它确实是在解决一个之前那两个词都解决不了的问题。&lt;/p&gt;
&lt;p&gt;所以今天这篇文章，我想把这三个词一次性给你讲透，主要回答三个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Harness Engineering 到底是个啥？&lt;/li&gt;
&lt;li&gt;它跟 Prompt Engineering、Context Engineering 是什么关系？&lt;/li&gt;
&lt;li&gt;OpenAI 和 Anthropic 实现 Harness 遇到了什么难点？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文章会有点长，但我尽量讲得通俗一点，你泡杯茶慢慢看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260613210511117.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;这个新词到底从哪儿冒出来的？&lt;/h2&gt;
&lt;p&gt;在开讲之前，我想先带你去「追个源」。&lt;/p&gt;
&lt;p&gt;因为我发现很多人在跟风聊 Harness Engineering 的时候，压根没搞清楚这个词最早是谁提出来的。等你知道了它是谁先喊出来的，你就会明白为啥它这次真的能火，而不是又一个换皮概念。&lt;/p&gt;
&lt;p&gt;Harness Engineering 这个词最早被正式叫响，是在 2026 年 2 月 5 号一篇博客里。作者叫 Mitchell Hashimoto。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776065806124-22e2c757-351a-4dbb-8778-a36af5b45d31.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;标题叫《My AI Adoption Journey》，中文大概是「我接纳 AI 的历程」。讲的就是他作为一个老派工程师，是怎么一步一步从「不信 AI」到「每天让 Agent 替我干活」的心路历程。&lt;/p&gt;
&lt;p&gt;他把整个过程拆成了 6 个 Step，前面几步都挺反直觉的，我挑几个有意思的给你讲讲。&lt;/p&gt;
&lt;p&gt;第一步，直接扔掉对话框。他说一上来别用 ChatGPT 那种聊天界面，要用就用能自主执行任务的 Agent。为啥？因为聊天界面会给你一种「我在和 AI 讨论」的幻觉，但实际上你啥也没干，Agent 才是真正让 AI 动起来的形态。只有把 Agent 放开让它自己执行，你才能真正感知到它的能力边界在哪儿。&lt;/p&gt;
&lt;p&gt;第二步更狠，他强迫自己用 Agent 去重做一部分自己早就已经手写过的代码。听起来是不是有点傻？明明自己两下就能写完的活，非要等 Agent 慢慢磨。他原话说这个过程「painful」，折磨人。但目的很明确：只有你亲自用 AI 去重做你已经熟得不能再熟的任务，你才能精准校准「它到底哪里行、哪里不行」。&lt;/p&gt;
&lt;p&gt;然后到了第五步，他给自己这套做法起了个名字，就叫 Engineer the Harness，打磨这套马具。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776065858145-d246c3c5-6702-41fc-a29b-16ad20b12f8b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;他给的定义特别简洁，我把原话翻译过来：&lt;/p&gt;
&lt;p&gt;每次当你发现 Agent 犯了一个错误，就花点时间去工程化一个解决方案，让它永远不会再犯同样的错误。&lt;/p&gt;
&lt;p&gt;你品品这个思路。绝大多数人遇到 Agent 犯错是怎么办的？骂两句，手动改掉，祈祷下次它别再犯。&lt;/p&gt;
&lt;p&gt;但 Mitchell 不是这么干的，他每次 Agent 犯错，都会停下来问自己：我能不能把这个错误永久性地修到环境里，让它下次在结构上就不可能再犯？&lt;/p&gt;
&lt;p&gt;可能是给 &lt;a href=&quot;http://AGENTS.md&quot;&gt;AGENTS.md&lt;/a&gt; 加一条规则，可能是加一个 linter，可能是补一个自动化测试，也可能是搞一个 Git Hook。反正关键是：这个修补必须沉淀到环境里，而不是留在人脑子里。&lt;/p&gt;
&lt;p&gt;这套做法的威力在于它是「复利」的。&lt;/p&gt;
&lt;p&gt;每一次 Agent 犯错，环境就会变强一点；环境变强一点，Agent 下一次就更少犯错；犯错变少，你改进的速度就更快。时间一长，你的 Harness 会越来越坚固，Agent 在你这个项目里会越跑越稳。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776066189480-9e2c02d4-abdb-4a92-aecb-4fc68b42eba8.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;博客发出来一周后，OpenAI 紧接着也发布了一篇官方博客，标题就叫《Harness engineering: leveraging Codex in an agent-first world》。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776065945432-58244ca8-d9b7-4d5a-9bdc-3f11f1c15c1c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这篇博客讲的是他们内部一个从 3 人起步的小团队，从一个空仓库出发，用 5 个月时间，靠 Agent 写出了 100 万行代码、合并了 1500 个 PR，全程没人手动写过一行代码。&lt;/p&gt;
&lt;p&gt;这篇博客一出，Harness Engineering 这个概念直接被推上了 AI 圈的风口浪尖。所以你能看明白这个词的路径了吧：&lt;/p&gt;
&lt;p&gt;一个基础设施圈的老法师先喊出来 → OpenAI 几天后发文背书 → 一周内整个 AI 圈开始刷屏。&lt;/p&gt;
&lt;p&gt;这种出身决定了它不会像很多 AI 新词一样「炒一波就凉」，它更像是一个在真实工程土壤里长出来的东西。&lt;/p&gt;
&lt;p&gt;好，交代完来源，咱们进入正题。&lt;/p&gt;
&lt;h2&gt;从提示词到 Harness&lt;/h2&gt;
&lt;p&gt;要真正讲清楚 Harness Engineering 在解决啥问题，咱们不能一上来就讲它。因为它不是凭空冒出来的新概念，而是 AI 工程这几年一步一步「被逼出来」的结果。&lt;/p&gt;
&lt;p&gt;你去复盘一下过去两年 AI 圈的变化就会发现一件特别有意思的事：AI 工程的重心，其实已经悄悄换过三次了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776066308212-34da4f03-3652-4447-9609-172f3afc4068.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一开始大家在聊的是 Prompt Engineering，提示词工程。那个时候所有人都相信一件事：模型不是不会，只是你没把话讲明白，只要提示词写得够好，啥都能搞定。&lt;/p&gt;
&lt;p&gt;后来大家发现事情不对劲。Agent 火起来之后，模型要做的事情越来越复杂，光「把话讲清楚」根本不够，它还得「知道」该用什么信息才行。于是 Context Engineering，上下文工程，接棒成为 AI 工程圈的新主角。&lt;/p&gt;
&lt;p&gt;但这事到这儿还没完。当大家把上下文管得越来越精细之后，又发现了一个更扎心的问题：信息都给对了，模型在长链路任务里还是会跑偏。&lt;/p&gt;
&lt;p&gt;提示词没毛病、上下文也没毛病，可它就是做不稳。正是在这个背景下，Mitchell Hashimoto 的那篇博客和 OpenAI 的那个百万行代码实验几乎同时出现，Harness Engineering 就这么被推到了台前。&lt;/p&gt;
&lt;p&gt;你有没有感觉出这条脉络？这三个阶段不是谁取代谁，而是每一步都在上一步撞到天花板之后，继续往前啃出来的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompt Engineering 解决的是：怎么让模型「听懂」你想干啥&lt;/li&gt;
&lt;li&gt;Context Engineering 解决的是：怎么让模型「知道」该用什么信息&lt;/li&gt;
&lt;li&gt;Harness Engineering 解决的是：怎么让模型在真实执行里「持续做对」一连串的事&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776066576145-0ecbdb76-7972-480f-b704-ad8d880db443.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;听起来三句话好像差不多对吧？但你一定要相信我，这三件事的难度和工作量，是层层放大的。&lt;/p&gt;
&lt;p&gt;把一句话讲明白难吗？不难。把一整套信息在合适的时机送给模型就开始有点门槛了。而要搭出一整套能让模型「持续稳定交付」的运行环境，那完全是另一个量级的工程活。&lt;/p&gt;
&lt;p&gt;这也顺便解释了一个很多人感到困惑的现象：同样是用 Claude 或者 GPT 这几个大模型，为啥有的团队做出来的 Agent 又稳又能打，到别人手里却一跑就跑偏？&lt;/p&gt;
&lt;p&gt;差的往往不是模型，而是&lt;strong&gt;围绕模型搭起来的那套东西，到底有没有做到位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776066415647-74305642-23d8-44b2-af00-cc2358cf4c56.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以接下来，我就按照「从提示词到 Harness」这条路径，带你一个阶段一个阶段往下走。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先看看 Prompt Engineering 当年解决了啥问题、又撞上了啥天花板；&lt;/li&gt;
&lt;li&gt;然后看看 Context Engineering 是怎么接棒的、又在哪儿遇到了新的瓶颈；&lt;/li&gt;
&lt;li&gt;最后再来讲 Harness Engineering 到底是怎么在这两个天花板之上，又往前顶了一大截。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;走完这条路，你对 Harness 到底是个啥，心里就会有一个非常清晰的底。&lt;/p&gt;
&lt;h3&gt;第一阶段：Prompt Engineering，先让模型「听懂」你&lt;/h3&gt;
&lt;p&gt;要讲清楚 Prompt Engineering，得先聊一个更基础的问题：大模型到底在干啥？&lt;/p&gt;
&lt;p&gt;你平时用 ChatGPT 也好，用 Claude 也好，看到的都是一个对话框，输入一句话，下面给你输出一段话。但如果把这层外壳扒掉，里面其实就一个东西：一个特别大的参数文件，存在硬盘上，加载到显卡的内存里。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776066677324-15ddd05e-f951-4648-a249-dd65fa7f3585.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个文件本身啥也不是。给它配一个 HTTP 接口，它就成了所谓的「大模型 API 服务」。给这个 API 套一个聊天界面，它就成了 ChatGPT；套一个代码编辑器，它就成了 Cursor、Trae 这种 AI IDE。&lt;/p&gt;
&lt;p&gt;那这个参数文件本身是怎么工作的呢？说白了就一件事：根据你输入的内容，预测下一个字最有可能是啥。&lt;/p&gt;
&lt;p&gt;注意我说的是「预测」，不是「思考」，这个区别很重要。它本质上是在猜你想要什么，然后把它觉得最可能的字一个一个往外吐。你给它的输入越模糊，它猜的范围就越大；你给它的输入越具体，它猜的范围就越收敛。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776066879420-d6ac518e-487c-4734-8552-f74bcbdd777e.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;知道了这个原理，你就能理解为啥同一个问题，换个说法效果能差十倍。&lt;/p&gt;
&lt;p&gt;举个例子。你丢一段代码给大模型，说「加个排序」。它会怎么回？大概率是给你回一段排序的代码片段，告诉你「就这么写」。但你拿到这段代码一看，傻眼了：它没说放在哪儿，也没说要不要改其他函数，更没把完整的代码给你。&lt;/p&gt;
&lt;p&gt;那如果你换一种说法呢？「这是我的完整代码，请帮我加上对 users 列表按年龄从大到小的排序，注意保留原有的所有逻辑，最后输出完整代码」。这次它给的结果一下就靠谱多了。&lt;/p&gt;
&lt;p&gt;为啥？因为大模型本质上是一个对上下文极度敏感的概率生成器。&lt;/p&gt;
&lt;p&gt;什么叫敏感？就是你给它什么样的输入，它就会沿着那个方向生成。&lt;/p&gt;
&lt;p&gt;你给它一个角色身份，比如「你是一个资深 Java 工程师」，它就会偏向用 Java 工程师的思路回答；你给它几个示例，比如「按照下面这个例子的格式来」，它就会沿着那个范式补全；你强调什么样的约束，比如「不要修改我已有的代码」，它就会把这个约束当成重点。&lt;/p&gt;
&lt;p&gt;你会发现，写提示词的本质，根本不是在「命令」模型，而是在塑造它的概率空间。你给它的每一句话，都在悄悄地把它的输出往某个方向推。&lt;/p&gt;
&lt;p&gt;正因为这种敏感性，大家慢慢摸索出了一套「提示词配方」。一个稍微正式一点的提示词，一般会包含这么几个部分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776067247821-7b5bfa65-9e8f-4499-b7b1-3f03593afbe0.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 角色设定：你是谁？比如「你是一个有十年经验的后端工程师」
- 背景信息：当前在干啥？比如「我正在用 Go 写一个订单系统」
- 参考资料：相关的文档、历史对话、代码片段
- 明确任务：具体要你干啥
- 约束条件：哪些不能做，比如「不要修改其他文件」
- 输出格式：希望以什么形式返回，JSON、Markdown 还是表格
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把这些东西按一定的结构组装起来，就是一份比较完整的提示词。这种有意识地去设计和调整提示词、让模型稳定输出你想要的内容的技术活，就叫 Prompt Engineering，提示词工程。&lt;/p&gt;
&lt;p&gt;它解决的核心问题，其实就一个：模型不是不会，而是你没把问题说明白。&lt;/p&gt;
&lt;p&gt;提示词工程在大模型刚火起来的那一两年，作用是非常大的。因为那个时候大家做的事情都很简单：聊天、写文案、翻译、问答。这种任务的特点是「短链路」，一句问一句答就完事了，提示词写好了基本就搞定。&lt;/p&gt;
&lt;p&gt;但很快，大家想做的事情变复杂了，提示词工程就开始撑不住了。&lt;/p&gt;
&lt;p&gt;举个例子。你让大模型帮你「分析一下我们公司去年的财报」，它再聪明，没看过你的财报，它能分析啥？你让它「按照公司内部的代码规范帮我写一个新功能」，它没看过你们的规范，它怎么知道该怎么写？你让它「在我们已有的几十个 API 之间做一些调用组合」，它根本不知道你有哪些 API。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776068391647-ba54093d-7359-4319-a948-bfd501d85ac3.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个时候你就会发现一个很尴尬的事实：提示词写得再漂亮，也替代不了「事实」本身。&lt;/p&gt;
&lt;p&gt;提示词擅长的，是把任务表达清楚、把约束讲明白，激发模型已有的能力。但它不擅长「凭空补出模型不知道的知识」，也不擅长「管理一大堆动态变化的信息」，更不擅长「在长链路任务里维持稳定的状态」。&lt;/p&gt;
&lt;p&gt;说白了，提示词工程解决的是「表达」的问题，不是「信息」的问题。&lt;/p&gt;
&lt;p&gt;于是第二阶段就来了：Context Engineering。&lt;/p&gt;
&lt;h3&gt;第二阶段：Context Engineering，让模型「知道」该用什么信息&lt;/h3&gt;
&lt;p&gt;为什么 Context Engineering 会突然火起来？因为大家做的产品形态变了。&lt;/p&gt;
&lt;p&gt;之前大家做的是聊天机器人，问一句答一句，链路短、状态少。&lt;/p&gt;
&lt;p&gt;但后来 Agent 火了，模型不再只是「回答问题」，而是要进到真实环境里去「干活」。它要多轮对话，要调用工具，要写代码，要查数据库，要在多个步骤之间传递中间结果，还要根据外部反馈不断修正自己的计划。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776068560240-5ac4fccb-167e-4ad2-8c0e-00059083d0c9.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个时候问题就完全变了。系统面对的已经不是「这一次回答对不对」，而是「整条链路能不能跑通」。&lt;/p&gt;
&lt;p&gt;举个例子。如果你不是简单地问一句「帮我总结这篇文章」，而是让大模型做一个更真实的任务，比如：「帮我分析这份需求文档，找出潜在风险，结合我们之前几次评审的意见，给出改进建议，最后生成一份发给产品经理的反馈稿」。&lt;/p&gt;
&lt;p&gt;你会发现，光靠一句提示词，根本完成不了这个任务。模型至少需要拿到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 当前的需求文档
- 历史的评审记录
- 公司的相关规范
- 当前任务的具体目标
- 之前已经分析出来的中间结论
- 收件人是谁、希望用什么语气
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些东西全部加起来，才叫一个完整的「任务环境」。&lt;/p&gt;
&lt;p&gt;正是基于这个观察，才慢慢形成了一个新的概念：Context（上下文）。&lt;/p&gt;
&lt;p&gt;什么叫上下文？我用一句最直白的话解释：&lt;/p&gt;
&lt;p&gt;每一次发给大模型的所有信息加在一起，就叫上下文。&lt;/p&gt;
&lt;p&gt;而我们之前聊的提示词，只是上下文里很小的一部分。一个完整的上下文还包括：用户输入、历史对话、检索到的资料、工具返回的结果、当前的任务状态、中间产物、系统规则、安全约束等等等等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776068927807-a66bed48-c129-43d7-95c8-808e4d176b98.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;理解了这一层，你就能理解 Context Engineering 是在干啥了。它的核心思想就一句话：&lt;/p&gt;
&lt;p&gt;模型未必知道，所以系统必须在合适的时机，把正确的信息送进去。&lt;/p&gt;
&lt;p&gt;你可能会想：那我把所有相关的资料一股脑全塞进去不就完事了吗？&lt;/p&gt;
&lt;p&gt;这里就有一个让人头疼的现实：大模型一次能处理的上下文是有上限的。这个上限叫「上下文窗口」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776069111967-392c2ddd-dba8-42cb-aff8-a1977f4104ae.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你可以把它想象成大模型的「短期记忆容量」。再聪明的模型，一次也只能记住这么多东西。&lt;/p&gt;
&lt;p&gt;更要命的是，就算窗口够大，大模型的注意力也不是均匀分布的。研究发现，当上下文塞得太满的时候，模型会出现一种叫「上下文腐化」的现象：它开始记不住前面的内容，开始前后矛盾，开始忽略你最初定下的规则。&lt;/p&gt;
&lt;p&gt;我自己感觉这个特别像一个被信息淹没的人：你给他太多东西要看，他反而抓不住重点。&lt;/p&gt;
&lt;p&gt;那怎么办？这就是 Context Engineering 要解决的核心问题：怎么在上下文窗口有限的前提下，把最相关的信息，以最合理的方式，送给模型。&lt;/p&gt;
&lt;p&gt;实践下来，它基本上可以拆成三个步骤：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776069438025-c6eaa14d-4fa3-4473-9503-2b903d234de8.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步，&lt;strong&gt;召回&lt;/strong&gt;。说白了就是「找信息」。从一大堆资料里找出跟当前任务最相关的那部分。这里面就涉及到大家熟悉的 RAG 技术：把你的所有文档切成小块，转成向量存起来，每次有问题进来，先去向量数据库里搜出最相关的几块。&lt;/li&gt;
&lt;li&gt;第二步，&lt;strong&gt;压缩&lt;/strong&gt;。找到的信息可能还是太多，那就压缩。怎么压？常见的做法是先让模型对每一段做个摘要，然后只把摘要送进最终的上下文。或者，对历史对话，把太老的对话压缩成一句话总结，只保留最近几轮的原文。&lt;/li&gt;
&lt;li&gt;第三步，&lt;strong&gt;组装&lt;/strong&gt;。压完之后还要按一定的顺序、一定的格式组装起来。为啥顺序很重要？因为大模型对「靠后的信息」更敏感。所以重要的指令、当前的任务，往往要放在靠后的位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776069596148-679989c8-254d-4443-ad10-227b42242a27.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不同的 AI 工具，这三步的实现各不相同。这就是为啥同样是用 Claude 模型，你用 Claude Code、Cursor、Trae 这些不同的 AI IDE，效果会差挺多。模型是同一个，但每家的上下文工程策略不一样，所以最后的体验也不一样。&lt;/p&gt;
&lt;p&gt;讲到这儿，我想专门聊一下 Anthropic 最近搞的 &lt;strong&gt;Agent Skills&lt;/strong&gt;，因为它特别能体现「上下文工程不是塞得越多越好」这个理念。&lt;/p&gt;
&lt;p&gt;很多人做 Agent 的时候，会犯这么一个错误：把所有可能用到的工具说明、所有 SOP、所有参考资料，一上来就一股脑塞进上下文。理论上，模型知道得越多越好，对吧？&lt;/p&gt;
&lt;p&gt;但实际上效果往往更糟糕。&lt;/p&gt;
&lt;p&gt;为啥？还是那句话：上下文窗口是稀缺资源，信息一多注意力就会被稀释。模型看到的东西一多，反而抓不住当前任务真正需要的那部分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776069694598-db92eda4-1af4-4e5a-a531-79bf11a14adb.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Agent Skills 提出的思路叫「&lt;strong&gt;渐进式披露&lt;/strong&gt;」：一开始只给模型看每个能力的「目录」，告诉它「我有这些工具、这些 SOP、这些参考」。&lt;/p&gt;
&lt;p&gt;等模型真的判断「我现在需要用某个工具」的时候，再把那个工具的详细说明、参数定义、使用示例动态加载进来。&lt;/p&gt;
&lt;p&gt;这个思路其实非常重要，它告诉我们：上下文优化的本质不是「给得更多」，而是「按需给、分层给、在正确的时机给」。&lt;/p&gt;
&lt;p&gt;讲到这儿，Context Engineering 的核心思想基本就讲完了。它在 Prompt Engineering 的基础上又往前走了一大步：从「把任务讲清楚」升级到了「把信息送对」。&lt;/p&gt;
&lt;p&gt;但你以为这就是终点了吗？&lt;/p&gt;
&lt;p&gt;不是。&lt;/p&gt;
&lt;h3&gt;第三阶段：Harness Engineering，让模型「做对」一连串的事&lt;/h3&gt;
&lt;p&gt;后来大家又发现了一个更麻烦的问题。&lt;/p&gt;
&lt;p&gt;你把提示词写得再漂亮，把上下文管理得再完美，模型在「单步」上的表现确实越来越好了。但只要任务的链路一长，模型就还是会出问题。&lt;/p&gt;
&lt;p&gt;什么问题？比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;计划做得很好，但执行的时候突然跑偏了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用工具调对了，但理解错了工具返回的结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在一个很长的任务链里，已经悄悄偏离初衷了，但系统完全没察觉&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;跑着跑着突然忘了自己最初要干啥&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776069924593-e0c18f62-1e77-430e-a3b4-941b77e085fd.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你仔细品品这个问题。提示词工程优化的是「意图的表达」，上下文工程优化的是「信息的供给」，但这两个其实都还停留在「输入侧」。&lt;/p&gt;
&lt;p&gt;而当模型真正开始「连续行动」的时候，会出现一个全新的问题：谁来监督它？谁来约束它？谁来在它跑偏的时候把它拉回来？&lt;/p&gt;
&lt;p&gt;这就是第三阶段要解决的事情。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Harness&lt;/strong&gt; 这个英文词，直译过来叫「马具」，或者说「缰绳」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776070020691-7675c625-fb16-4d85-8b13-64856be30ccb.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为啥用这么一个词来命名一个 AI 工程概念？想象一下你骑马的场景：马本身有强大的力量，能跑能跳能驮东西，但如果没有缰绳和马具，这股力量就是失控的。马可能往悬崖上跑，可能甩你下来，可能跑去吃草不回来了。马具的作用，就是让这股力量为你所用。&lt;/p&gt;
&lt;p&gt;放在 AI 系统里，这个比喻就特别贴切了。&lt;strong&gt;当模型从「回答问题」走向「执行任务」，系统就不能只负责喂信息，还要能「驾驭」整个执行过程。这就是 Harness Engineering 的出发点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果说前两代工程关注的是「怎么让模型更会想」，那 Harness 关注的就是「怎么让模型不跑偏、跑得稳、出了错还能爬起来」。&lt;/p&gt;
&lt;p&gt;讲到这儿，我用一个特别通俗的例子，把这三个阶段的区别一次性给你讲清楚。&lt;/p&gt;
&lt;p&gt;假设你是一个销售经理，要派一个刚入职的新人去做一次很重要的客户拜访。&lt;/p&gt;
&lt;p&gt;你会做哪些事情呢？&lt;/p&gt;
&lt;p&gt;第一件事，把任务讲清楚。你会告诉他：「见到客户先寒暄，然后介绍方案，再问需求，最后确认下一步」。这就是 Prompt Engineering，重点是把话说明白，不让新人懵。&lt;/p&gt;
&lt;p&gt;第二件事，把资料准备齐全。光把流程讲清楚不够，新人还得知道：客户是谁？之前聊过啥？产品报价多少？竞品啥情况？这次会议的目标是啥？这些资料你都得给他。这就是 Context Engineering，重点是把信息供给到位。&lt;/p&gt;
&lt;p&gt;第三件事呢？如果这是一个特别重要的客户，你光把流程讲清楚、资料给齐了，你心里还是不踏实对吧？你还会做更多的事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;让他带一份 checklist 去，每个关键节点都要打勾&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让他在关键节点实时给你汇报&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让他录音，回来你要复盘&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果发现他拜访过程中跑偏了，马上电话纠正&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拜访完成后，要按照明确的标准去验收成果&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一整套东西，就是 Harness。它的重点已经不是「把话讲清楚」「把资料给齐」了，而是「有没有一整套机制能持续观测、持续纠正、最终验收」。&lt;/p&gt;
&lt;p&gt;是不是一下就清楚了？&lt;/p&gt;
&lt;p&gt;关于 Harness 的边界，圈子里流传着一个特别简洁的等式：&lt;/p&gt;
&lt;p&gt;Agent = Model + Harness&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776070193737-2c4cf3ec-baff-41e1-a4c7-63345f1bd7d5.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;翻译成人话：在一个 AI Agent 系统里面，除了模型本身之外，几乎所有决定它能不能稳定交付的东西，都属于 Harness。&lt;/p&gt;
&lt;p&gt;所以你也可以反过来推：&lt;/p&gt;
&lt;p&gt;Harness = Agent − Model&lt;/p&gt;
&lt;p&gt;这个公式我特别喜欢，因为它一下就把 Harness 的边界划清楚了。&lt;/p&gt;
&lt;p&gt;讲到这儿你可能会问：那是不是 Harness Engineering 出来了，前面那两个就过时了？&lt;/p&gt;
&lt;p&gt;不是的。这三者根本不是替代关系，而是包含关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776070304473-2e6643f9-5d7b-4e76-9c81-1f1ef174ef4f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Prompt 是对「指令」的工程化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Context 是对「输入环境」的工程化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Harness 是对「整个运行系统」的工程化&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们的边界一层比一层大。Prompt 是 Context 的一部分，Context 是 Harness 的一部分。当你做 Harness 的时候，里面一定包含 Context 工程，Context 工程里又一定包含 Prompt 工程。&lt;/p&gt;
&lt;p&gt;所以 Harness Engineering 不是来取代谁的，它是站在更大的系统视角上，把前面这两个都包进去了。&lt;/p&gt;
&lt;p&gt;好，到这里 Harness Engineering 是啥、和前面两个啥关系，就基本讲完了。但只懂概念肯定不够，下一章我要把它拆开，看看一个成熟的 Harness 到底包含哪些东西。&lt;/p&gt;
&lt;h2&gt;Harness 拆开看，里面到底装了些啥？&lt;/h2&gt;
&lt;p&gt;如果你去看 OpenAI、Anthropic、LangChain 这些做 Agent 的顶级团队，会发现一件挺有意思的事：他们的产品形态不一样、技术栈不一样、服务对象也不一样，&lt;strong&gt;但你把他们的 Harness 掀开看内部结构，里面的组件居然惊人地相似&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这不是巧合。而是因为「让一个 Agent 在真实世界里稳定工作」这个命题，天然就会推着所有人往同一个方向收敛。一个成熟的 Harness 大致可以拆成六块核心组件，我把它们叫做六层，每一层都在解决「驾驭」这件事的一个独立维度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776070595938-1a571d50-6434-4e50-99e4-bb1da82904f2.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这六层看起来有点多，但其实可以按「它在干啥」分成三组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输入侧&lt;/strong&gt;（让模型看到正确的东西）：上下文精细化管理 + 记忆与状态管理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;动作侧&lt;/strong&gt;（让模型做出正确的事）：工具系统 + 任务执行编排&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;校验侧&lt;/strong&gt;（让模型知道做没做对 + 出错能爬起来）：评估观测 + 约束恢复&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你发现没有？这三组其实对应了一个工程师在真实环境里干活的三个必要条件：&lt;strong&gt;看得准 → 做得对 → 错了能兜底&lt;/strong&gt;。任何一个成熟的 Harness，你去拆一拆，都能找到这三组六层的影子。&lt;/p&gt;
&lt;p&gt;在真正展开六层之前，我想先把每一层对应的那个「要解决的核心问题」用一张表提前摆到你面前。这张表你可以当成整章的路线图，后面每读完一层，回头瞄一眼，就知道自己读到哪儿、哪些还没读：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;这一层在解决的一句话问题&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;上下文精细化&lt;/td&gt;
&lt;td&gt;模型&lt;strong&gt;这一轮&lt;/strong&gt;该看到什么？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具系统&lt;/td&gt;
&lt;td&gt;模型&lt;strong&gt;用什么&lt;/strong&gt;动手？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行编排&lt;/td&gt;
&lt;td&gt;模型&lt;strong&gt;下一步&lt;/strong&gt;该干啥？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;记忆与状态&lt;/td&gt;
&lt;td&gt;模型&lt;strong&gt;跨轮&lt;/strong&gt;该记住什么？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;评估与观测&lt;/td&gt;
&lt;td&gt;模型&lt;strong&gt;做得好不好&lt;/strong&gt;有没有尺子？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;约束与恢复&lt;/td&gt;
&lt;td&gt;模型&lt;strong&gt;出错了&lt;/strong&gt;能不能爬起来？&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;你看这六个问题，它们根本不是什么技术理论的包装，而是一个真实工程师在真实环境里做事情的时候，本能会问自己的那六件事。这也是为啥 OpenAI、Anthropic、LangChain 这些完全不同的团队，最后都不约而同地收敛到差不多的结构上。&lt;strong&gt;这六层不是谁凭空发明的，是从现实里长出来的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了让这六层不至于变成抽象的概念堆，我决定挑一个贯穿整章的具体例子。后面每讲一层，我都会回到这个例子里给你一个具体画面。例子是这样的:&lt;/p&gt;
&lt;p&gt;🔧 假设你要做一个 Agent，任务是：&lt;strong&gt;每天定时帮你扫一遍 GitHub 上你关注的仓库的新 PR，从里面挑出值得你特别关注的那几个，对每一个生成一段简短的摘要和点评，最后把结果发到你的 Slack&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;好，带着这个 PR Review Agent，我们一层一层往下看。&lt;/p&gt;
&lt;h3&gt;第一层，上下文的精细化管理&lt;/h3&gt;
&lt;p&gt;先说输入侧的第一件事：&lt;strong&gt;模型每次被调用的时候，到底该看到什么&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;这里有个特别关键的修饰词，「每次被调用的时候」。我专门强调它是因为这一层很容易和第四层（记忆与状态）搞混。简单区分一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776070946961-03aae307-8d3e-4a4b-8323-cd70a66422c1.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第一层（上下文的精细化）管的是「空间」&lt;/strong&gt;：这一轮发给模型的那一坨上下文，长啥样、装了些啥、怎么排布&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第四层（记忆与状态）管的是「时间」&lt;/strong&gt;：上一轮发生过的事情，怎么流动到下一轮&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以把它想成一台相机：第一层是「取景框」，第四层是「胶卷」。两件事都重要，但看的方向完全不同。&lt;/p&gt;
&lt;p&gt;回到我们的 PR Review Agent。它每处理一个 PR 的时候，到底该看到什么？最粗暴的做法是把整个 PR 的 diff 全扔给它，这恰恰是最容易犯的错。它真正需要的其实是一组&lt;strong&gt;精挑细选的信息&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PR 的标题和描述&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;diff 里真正被改动的那几个文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这些文件在仓库里对应的模块说明&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作者最近几次提交的风格偏好&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仓库的 code review 惯例&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;你塞给它越多无关信息，它的注意力就越散&lt;/strong&gt;。这个现象 Anthropic 在他们的博客里专门命名了，叫「context rot」（上下文腐化）。他们给的解法是「just-in-time retrieval」，也就是让 Agent 边干活边按需抓信息，而不是一上来就把所有可能有用的东西一股脑塞进去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776070986089-ac7fa2b3-a1de-4549-8d28-6ba88f0b76e1.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以这一层的核心工作可以浓缩成三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一，&lt;strong&gt;把角色和目标钉死&lt;/strong&gt;。模型得知道自己是一个「PR 审查助手」，当前任务是「挑出值得关注的 PR 并生成摘要」，成功标准是「我挑出来的真的都是该被关注的」。大部分 Agent 跑偏，根源就是这一步没说清楚。&lt;/li&gt;
&lt;li&gt;第二，&lt;strong&gt;动态筛选而不是一次塞满&lt;/strong&gt;。只把当前这个 PR 相关的那几块信息拉进来，其余的留在文件系统里，等需要了再取。&lt;/li&gt;
&lt;li&gt;第三，&lt;strong&gt;结构化组织&lt;/strong&gt;。固定规则（code review 惯例）放一处，动态证据（当前 PR 的内容）放一处，中间结论（我对这个 PR 的初步打分）放一处，三者要分开。否则模型会「自我污染」，也就是用前面错的中间结论去影响后面的判断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第二层，工具系统的可控调用&lt;/h3&gt;
&lt;p&gt;聊完「看到什么」，再聊「能做什么」。&lt;/p&gt;
&lt;p&gt;没有工具的大模型本质上还是一个文本预测器。它能告诉你一个 PR 应该怎么审，但它没法&lt;strong&gt;真的去 GitHub 上读那个 PR&lt;/strong&gt;；它能写出一段 Slack 消息的文本，但它没法&lt;strong&gt;真的把这段消息发到你的频道&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776071140455-08783445-bed7-42c5-9e06-2efe50301b1b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接上工具之后，PR Review Agent 才真正活过来。但工具不是接得越多越好。OpenAI 在做 Codex 早期踩过这个坑：他们一开始给 Agent 接了一堆工具，想着「选择多总是好的」，结果 Agent 频繁用错工具、用错时机。后来砍掉一大半，效果反而上去了。&lt;/p&gt;
&lt;p&gt;所以这一层你要回答三个问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;给它哪些工具？&lt;/strong&gt;&lt;br /&gt;
对 PR Review Agent 来说，至少需要四件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;gh&lt;/code&gt; 命令行工具：拉 PR 列表、看 diff&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读文件工具：看仓库里的相关代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码搜索工具：查某个函数在哪儿被用到&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Slack 发送工具：把结果发出去&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;别的工具，比如「重跑 CI」「直接给 PR 打标签」，除非真的必要，先别加。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么时候用哪个工具？&lt;/strong&gt;&lt;br /&gt;
该查的时候要查，不该查的时候别瞎查。比如 Agent 判断「这个 PR 改的函数是不是核心逻辑」的时候应该去代码搜索，而不是凭感觉猜。反过来，明明 diff 已经在上下文里了，再去重新拉一次 PR 纯属浪费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工具结果怎么喂回模型？&lt;/strong&gt;&lt;br /&gt;
这条最容易被忽略。比如 Agent 调用代码搜索，拿到 30 条匹配。你是不是要把 30 条原文原样塞回去？不是。你要先做一层提炼，比如只留核心模块的那几条，再喂回去。否则这 30 条原文一进来，上下文又被污染了。&lt;/p&gt;
&lt;p&gt;你最近听得很多的 &lt;strong&gt;MCP（Model Context Protocol）&lt;/strong&gt;，本质上就是在做工具层的标准化，让任何工具都能用同一种方式接到任何 Agent 上，大家不用再各自重复造轮子。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776071263582-f1ffc8fe-f37f-48fe-bcd4-812cfeb91a3b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;第三层，任务执行的全局编排&lt;/h3&gt;
&lt;p&gt;工具接上了，模型就能动手了。但能动手不等于能做成事。&lt;/p&gt;
&lt;p&gt;这里我直接把一句你需要记住的话摆出来：&lt;strong&gt;Agent 的本质，说白了就是一个 for 循环&lt;/strong&gt;。思考一步 → 行动一步 → 观察结果 → 再思考下一步。这个循环结构有个很经典的名字叫 &lt;strong&gt;ReAct&lt;/strong&gt;（Reasoning + Acting）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776071434368-28346fac-11e1-483a-9df1-d2ec3c2581c7.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;听起来很朴素对吧？但魔鬼就藏在这个循环里。&lt;/p&gt;
&lt;p&gt;Agent 经常翻车的场景是：&lt;strong&gt;每一步它都会做，但把所有步骤串起来之后就不会了&lt;/strong&gt;。它会拉 PR 列表，会读 diff，会写摘要，但它不知道应该先拉全列表再逐个分析，还是应该边拉边评，最后交付给你经常就是一堆半成品。&lt;/p&gt;
&lt;p&gt;这就是第三层的职责：&lt;strong&gt;给模型一条明确的工作轨道&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;回到 PR Review Agent。它的工作轨道应该是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 拉取仓库当前所有开放的 PR 列表
2. 对每一个 PR：
   a. 读 diff 和描述
   b. 判断涉及的是核心模块还是边缘模块
   c. 核心模块的 PR 做深度分析
   d. 给出一个重要性打分（1-5）
3. 按重要性排序，选前 3 个
4. 为每一个生成摘要和点评
5. 汇总发到 Slack
6. 检查发送是否成功，失败则重试
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这条轨道，Agent 就知道「我现在在哪一步，下一步该干啥」。它不会再瞎跑。&lt;/p&gt;
&lt;p&gt;除了 ReAct 之外，还有几个业界常见的编排模式你可以记一下名字：&lt;strong&gt;Plan-and-Execute&lt;/strong&gt;（先规划完整计划再执行，适合长链路任务）、&lt;strong&gt;Reflexion&lt;/strong&gt;（每次失败都让 Agent 反思一下再重试）、&lt;strong&gt;Tree of Thoughts&lt;/strong&gt;（同时探索多条思路再选最好的）。不同场景会用不同的编排策略。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776071659343-f2921e9b-fd39-4f3c-83af-2414e0cfb26d.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;第四层，记忆与状态的分层管理&lt;/h3&gt;
&lt;p&gt;现在聊时间侧：Agent 怎么在「跨轮」之间保持连贯？&lt;/p&gt;
&lt;p&gt;没有状态管理的 Agent，每一轮调用之间都是失忆的。你的 PR Review Agent 今天跑了一遍，明天再跑的时候完全不记得「这个 PR 昨天已经审过了」，于是又审一遍再发一次消息，Slack 频道很快就会变成重复消息的坟场。&lt;/p&gt;
&lt;p&gt;这就是为啥 Harness 必须管状态。&lt;/p&gt;
&lt;p&gt;这里要回扣我们开头讲的 Mitchell Hashimoto 和 Anthropic 的一个核心洞察：&lt;strong&gt;Agent 的状态不应该放在上下文窗口里，而应该外化到文件系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Anthropic 在《Effective harnesses for long-running agents》里给出的具体做法是让 Agent 维护一个 &lt;code&gt;claude-progress.txt&lt;/code&gt; 日志、一个 &lt;code&gt;init.sh&lt;/code&gt; 启动脚本、加上完整的 git history，作为「长期记忆介质」。下一轮换一个全新的、干净的上下文窗口接手时，从这些文件里一读，立刻就知道「现在到哪一步了」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776072006122-7141789b-1efe-49df-be3c-a9f146394efc.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;放到 PR Review Agent 上，你可以做同样的事。它需要管的状态至少有三类，&lt;strong&gt;必须分层存&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;任务状态&lt;/strong&gt;：今天已经处理到哪个 PR 了？还剩几个？每个的打分是多少？这类信息写在一个 &lt;code&gt;today-progress.json&lt;/code&gt; 里，当天任务跑完就归档&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;会话中间结果&lt;/strong&gt;：当前这一轮里 Agent 对某个 PR 做出的初步判断。这类信息随会话结束就可以丢，不用持久化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;长期记忆和用户偏好&lt;/strong&gt;：你喜欢关注什么类型的 PR？你特别看重哪些模块？这类信息写在常驻的 &lt;code&gt;user-preferences.md&lt;/code&gt; 里，每次调用都注入&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你发现没有？这三类记忆的生命周期完全不同：任务状态活到任务结束，会话中间结果活到当轮结束，长期记忆跨所有任务存在。混在一起就乱了，&lt;strong&gt;分清楚才能用好&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;顺便说一句，Claude Code、Cursor、Trae 这些 AI IDE 里的规则文件（&lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt; / .cursorrules），就是「长期记忆」这一类的典型实现。&lt;/p&gt;
&lt;p&gt;每次调用都自动注入，Agent 永远「记得」项目的核心约束。这一层我们在后面的落地章节还会具体动手写。&lt;/p&gt;
&lt;h3&gt;第五层，独立的评估与观测体系&lt;/h3&gt;
&lt;p&gt;这一层是&lt;strong&gt;最容易被跳过、但跳过之后就进退两难&lt;/strong&gt;的一层。&lt;/p&gt;
&lt;p&gt;先看一个真实场景。我见过太多团队，做出一个 Agent 之后高高兴兴上线，结果跑了两周才发现：这玩意儿的实际成功率只有 50%。&lt;/p&gt;
&lt;p&gt;不是它不出结果，而是它每次都出结果，但一半时候是错的。问题在于这两周里没人发现，因为根本没有机制能告诉团队「它这次到底做得对不对」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776072121344-e49b464f-d299-47a5-9951-a06406e7bd84.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这就是没有第五层的下场。&lt;/p&gt;
&lt;p&gt;那第五层到底要做什么？我把它拆成两件事：一件是&lt;strong&gt;有个尺子&lt;/strong&gt;，另一件是&lt;strong&gt;能看到每一次的量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;尺子：Eval 集（这一层真正的核心）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Eval 集（evaluation set）是做 Agent 开发的业界标准做法，也是这一层的灵魂。&lt;/p&gt;
&lt;p&gt;简单说就是：&lt;strong&gt;你手写一批典型任务，每一个都标注好「正确答案长啥样」&lt;/strong&gt;，然后每次你对 Harness 做了任何改动（比如改了 &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt;、加了一个新工具、调整了编排流程），都让 Agent 把这批任务再跑一遍，对比成功率。&lt;/p&gt;
&lt;p&gt;对 PR Review Agent 来说，一个最小可用的 Eval 集可能是这样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从过去三个月挑 20 个真实 PR&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每一个都标注「是不是重要」「摘要应该怎么写」&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次改完 Agent 就跑一遍这 20 个，看它挑对了几个、写对了几个&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没有这个 Eval 集，你对 Agent 好不好的判断永远停留在「我感觉这次变好了」的玄学阶段。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LangChain 把他们的 Terminal Bench 成绩从 52.8 推到 66.5 分、直接从榜单 30 开外冲到前 5，靠的就是基于 Eval 集做 trace 回放和迭代&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Anthropic 和 OpenAI 内部同样有大量的 Eval 基础设施。这不是锦上添花的可选项，是工程级 Agent 的必需品。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;量：Trace + 日志 + 指标&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有了尺子还不够，你还得看到 Agent 每一次的&lt;strong&gt;真实足迹&lt;/strong&gt;，也就是它每一步做了什么决策、调了哪个工具、拿到什么返回、花了多少 token。&lt;/p&gt;
&lt;p&gt;这就是 LangSmith、Langfuse 这类 trace 系统存在的意义。能看到 trace，你才能定位失败那一步发生了什么，才能往 Harness 里补上对应的修复。&lt;/p&gt;
&lt;p&gt;这一层做到位之后，你对 Agent 的调试就从「猜」变成了「看」。&lt;/p&gt;
&lt;h3&gt;第六层，约束校验与失败恢复机制&lt;/h3&gt;
&lt;p&gt;最后一层。在真实环境里，&lt;strong&gt;失败不是例外，是常态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你的 PR Review Agent 真跑起来之后，一定会遇到各种奇形怪状的失败：GitHub API 限流、某个 PR 的 diff 太大把上下文冲爆、Slack webhook 过期、Agent 误判了一个无关 PR 然后疯狂读代码把 token 耗光一半预算……如果没有恢复机制，每次失败都是从头再来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776072377559-5810252a-d77a-4552-a3a9-301139af5c27.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这一层要做三件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;约束：定义「什么事 Agent 不能做」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对 PR Review Agent 来说，约束可以包括：「一次最多分析 20 个 PR」「不能对已 closed 的 PR 再评论」「不能直接修改 PR 本身」「token 用量超过 10 万就立刻停下」。这些约束最好&lt;strong&gt;硬编码到代码或 linter 规则里&lt;/strong&gt;，而不是写在提示词里靠 Agent 自己遵守。OpenAI 在 Codex 项目里把资深工程师的经验固化成他们叫做「Golden Principles」的机制（我们在下一章会专门展开讲），就是这种思路的极致版。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;校验：在每一步输出前后都做自动检查&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如 Agent 给出摘要后先跑一道格式校验（是不是 Markdown？几个段落都在？），发送到 Slack 前先检查频道名是不是在白名单里。&lt;strong&gt;校验不是审美品味，是硬规则&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;恢复：失败之后有预案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GitHub 限流 → 等一段时间后重试；Slack 发送失败 → 先落到本地队列，下次重试；token 快耗光 → 立即停下并保存进度，下一轮继续。&lt;strong&gt;每一种典型失败都应该有一条明确的恢复路径&lt;/strong&gt;，而不是一股脑全挂掉。&lt;/p&gt;
&lt;p&gt;这三件事加起来，才能让 Agent 从「能跑」升级到「能在生产环境跑」。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;讲到这儿，六层就全部展开完了。现在你可以再回去翻一眼章节开头那张「六问对应表」，把每一层和它对应的那个问题在心里重新对一遍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776070595938-1a571d50-6434-4e50-99e4-bb1da82904f2-20260613210223616.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这一轮你对这六个问题的理解，应该和一开始看的时候完全不一样了，那些问题背后的门道你都见过了。&lt;/p&gt;
&lt;p&gt;还记得开头那一章我们讲过 Mitchell Hashimoto 的「复利效应」吗？他说 Harness 的核心是「每次 Agent 犯错，都把修复沉到环境里」。那个修复到底沉到哪儿？&lt;strong&gt;答案就是这六层的其中某一层&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;你发现 Agent 总是漏掉某个上下文信息 → 去改第一层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你发现它总是用错工具 → 去改第二层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你发现它步骤乱 → 去改第三层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你发现它跨天记不住进度 → 去改第四层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你发现你没法判断它做得好不好 → 去搭第五层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你发现它一失败就崩溃 → 去强化第六层&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这六层不是一张「必须一次搭完」的任务清单。&lt;strong&gt;它是一张路标，告诉你下一次 Agent 犯错时，你的修复应该落到哪里&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;随着时间推移，这六层的每一层都会被你一点一点填充、加固、打磨，你的 Harness 就是这样一寸一寸长大的。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;讲到这儿，Harness 装了些啥你心里应该有谱了。&lt;/p&gt;
&lt;p&gt;但你肯定还有疑问：这些东西听起来都对，但大厂真的是这么做的吗？具体是怎么落地的？&lt;/p&gt;
&lt;p&gt;下一章我就来讲这个：抓 5 个真实的工程难题，看看大厂是怎么解的。&lt;/p&gt;
&lt;h2&gt;Harness 有什么难点？&lt;/h2&gt;
&lt;p&gt;讲完六层能力的骨架，你可能会觉得这套东西好像挺有体系的。但真要动手做，各大公司都会告诉你一个共同的感受：&lt;strong&gt;概念清晰是一回事，落地是另一回事&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下面我挑 5 个大厂在真实项目里踩过的、特别典型的坑给你看看。&lt;/p&gt;
&lt;p&gt;你会发现 Harness 真正的难度根本不在蓝图，而在这些具体的细节里。每个难题我都会用同一种结构来讲：先看现象是啥，再看大厂是怎么反常识地解的，最后提炼出一条你可以直接记住的原则。&lt;/p&gt;
&lt;h3&gt;难题一，Agent 跑久了为啥会越走越偏？&lt;/h3&gt;
&lt;p&gt;这是几乎所有做长链路 Agent 的团队都会遇到的问题。&lt;/p&gt;
&lt;p&gt;现象是这样的：一开始 Agent 表现挺好，目标清晰，步骤明确。但跑着跑着，你会发现它开始「忘」。它忘了最初定的目标，忘了之前已经做过的决定，开始重复劳动，开始偏离主线。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776072754750-bff70e70-5a4b-4a4e-95ba-5e6c76135bc2.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;更诡异的是，Cognition（就是做 Devin 那家公司）在用 Claude Sonnet 4.5 重做 Devin 的时候，观察到一个特别有意思的现象，他们把它叫做「&lt;strong&gt;上下文焦虑&lt;/strong&gt;」（Context Anxiety）。&lt;/p&gt;
&lt;p&gt;什么意思呢？就是模型自己好像也能感觉到「我快撑不住了」。当它觉得上下文窗口快用完的时候，模型不仅开始丢细节，还会出现一种奇怪的行为：它开始着急地想收尾。它会突然简化方案、跳过验证步骤、急匆匆地宣布「任务完成」。&lt;/p&gt;
&lt;p&gt;更神奇的是，研究发现模型对自己「还剩多少上下文」的估计非常不准，经常以为自己快没空间了，其实还剩一大半。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776073011838-76664ada-96e7-4fd8-bb71-1e1e1084337f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你品品这个现象，是不是特别像一个被任务压垮的人？&lt;/p&gt;
&lt;p&gt;很多团队遇到这个问题，第一反应是做「上下文压缩」（Context Compaction）：把前面的历史压成摘要，腾出空间继续跑。&lt;/p&gt;
&lt;p&gt;这个思路对不对？对，但 Anthropic 在另一篇《Harness design for long-running application development》博客里挑明了一个更扎心的观察：&lt;strong&gt;光压缩根本不够&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;他们确认了 Sonnet 4.5 确实存在前面说的那种「上下文焦虑」倾向，一旦这种状态上来了，只压缩历史、不把整个上下文窗口彻底换掉，那种「已经累了」的负担感模型还是带着，Agent 还是会在长链路任务里慢慢失焦。&lt;/p&gt;
&lt;p&gt;真正解开这个结的关键动作，Anthropic 把这个做法叫做 &lt;strong&gt;Context Reset&lt;/strong&gt;：直接把旧的上下文窗口整个丢掉，换一个干净的接手。&lt;/p&gt;
&lt;p&gt;那 Context Reset 具体怎么落地？&lt;/p&gt;
&lt;p&gt;Anthropic 在他们的另一篇《Effective harnesses for long-running agents》博客里给出了一套具体做法：&lt;strong&gt;让 Agent 跨多轮接力跑，状态全部外化到文件系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776073327524-adb29655-f0f5-47fe-922c-ab00c5942c18.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体怎么做？整个系统其实&lt;strong&gt;只有一个 Agent&lt;/strong&gt;，系统提示词、工具集、整套 harness 全都一样。&lt;/p&gt;
&lt;p&gt;真正在变的只是每一轮的初始 user prompt：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一轮&lt;/strong&gt;用一个专门的「初始化」prompt，让 Agent 把环境信息、项目状态、约束条件整理好，写到一份 &lt;code&gt;claude-progress.txt&lt;/code&gt; 日志、一份 &lt;code&gt;init.sh&lt;/code&gt; 启动脚本、一个初始 git commit 里；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后续每一轮&lt;/strong&gt;用一个专门的「增量推进」prompt，让它做一点进展，然后把新状态再写回这几份文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关键就在这里：&lt;strong&gt;每一轮开始的时候，Agent 都面对一个完全干净的上下文窗口&lt;/strong&gt;，它根本不记得上一轮的对话历史，它靠的完全是读取文件系统里这几份「交接文档」来恢复「我现在在哪一步」。&lt;/p&gt;
&lt;p&gt;Anthropic 在博客里为了好讲，把这两种 prompt 启动的 Agent 分别叫 initializer agent 和 coding agent，但他们也在脚注里专门澄清了：&lt;strong&gt;这其实是同一个 Agent，只是首轮和后续轮次用了不同的 user prompt 启动而已&lt;/strong&gt;。真正被「换掉」的不是 Agent，是上下文窗口。&lt;/p&gt;
&lt;p&gt;你品品这个架构。它的关键不在「压缩上下文」，而在&lt;strong&gt;把状态外化到文件系统&lt;/strong&gt;。文件系统变成了真正的长期记忆，上下文窗口本身只负责处理当前这一轮，处理完就可以整个丢掉，而整个任务的进度完全不丢。&lt;/p&gt;
&lt;p&gt;这特别像我们在工程里遇到内存泄漏的时候是怎么做的。你不会拼命去优化内存，你会直接重启进程，把状态从磁盘恢复出来。Agent 的长期运行，用的是同一套思路。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原则一：重启胜过修补，状态沉到文件里，Agent 随时可以在一个干净的上下文窗口里接力继续&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;难题二，让 Agent 自己给自己打分，为啥总偏乐观？&lt;/h3&gt;
&lt;p&gt;这是另一个特别隐蔽的问题。&lt;/p&gt;
&lt;p&gt;很多人做 Agent 的时候是这样的：让 Agent 干活，干完之后再让它自己评估「做得怎么样」。看起来挺合理对吧？让它有一个自我反思的环节。&lt;/p&gt;
&lt;p&gt;但实际效果呢？Agent 永远觉得自己干得不错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776073460829-6d1c584e-e4b3-4f47-98c1-72d5e3ef231f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;特别是在那些没有标准答案的任务上，比如「设计一个用户界面」「写一篇有说服力的文案」「评估这段代码的可读性」，自评的偏差会特别明显。它会自动忽略自己做得不好的部分，然后给自己打一个还不错的分。&lt;/p&gt;
&lt;p&gt;为啥会这样？想想咱们人就理解了。让一个人自己给自己打绩效，他会公平吗？很难。&lt;/p&gt;
&lt;p&gt;Anthropic 后来想明白了一件事：让干活的和验收的，必须是不同的人。&lt;/p&gt;
&lt;p&gt;所以他们在另一篇《Harness design for long-running application development》里搞出了一个三角分工：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776073845850-c414e8e2-9cb8-4f85-8842-bd8cc7876468.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Planner（规划者）&lt;/strong&gt;：负责把模糊的需求扩展成完整的规格说明&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generator（生成者）&lt;/strong&gt;：负责一步一步去实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Evaluator（验收者）&lt;/strong&gt;：负责像 QA 一样真实地测试&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更关键的是，Evaluator 不是简单地看一眼代码就完事，它必须真的去操作页面、看具体的交互、检查实际的运行结果。这就保证了它的验收是有「真实环境」托底的，而不是抽象地 review。&lt;/p&gt;
&lt;p&gt;只要这三个角色足够独立，系统就能形成一个真正有效的循环：规划 → 生成 → 验收 → 修复 → 再验收。这才是闭环。不要让一个 Agent 既当运动员又当裁判，也别让它既是厨师又是食客。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原则二：生产和验收必须分离，而且验收方必须能摸到真实世界&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;难题三，Agent 总是失败，工程师到底该干啥？&lt;/h3&gt;
&lt;p&gt;这个难题比前两个都更深，因为它动的不是解法，是&lt;strong&gt;工程师自己的角色定位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;先说现象。当 Agent 反复失败的时候，一般人遇到的本能反应只有两个：要么再调调提示词，要么换个更强的模型。&lt;/p&gt;
&lt;p&gt;但 OpenAI 在做 Codex 项目的时候，实践告诉他们：&lt;strong&gt;工程师本能反应的这两招，其实都是错的方向&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;他们干了一件在传统程序员看来非常离谱的事：在 Codex 那个百万行代码的项目里，&lt;strong&gt;人类工程师几乎不写一行代码，全部由 Agent 来写&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776074935995-df106b99-f59f-43b3-a6e4-f244605192dc.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那人类工程师到底在干啥？从他们这套实践里，你能看到工程师的工作重心其实都压在了三件事上面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;把产品目标拆解成 Agent 能力边界内的小任务&lt;/strong&gt;，确保每一件事都是 Agent 接得住的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当 Agent 反复失败时，不是去催它「再努力一点」，而是去看它「环境里缺了什么能力」，然后把那个能力补进环境里&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;建立反馈链路，让 Agent 真正能看到自己工作的结果&lt;/strong&gt;，而不是两眼一抹黑地瞎跑&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你看出第二条的意思了吗？这其实是一次思维方式的根本转变。&lt;/p&gt;
&lt;p&gt;以前遇到 Agent 写代码有 bug，传统做法是加一句提示词「请仔细检查代码不要有 bug」，然后祈祷模型这次听话。&lt;/p&gt;
&lt;p&gt;而 Codex 团队的做法是：&lt;strong&gt;给 Agent 接上 lint、单测、运行环境，让它自己写完自己跑，看见 bug 自己改&lt;/strong&gt;。同样一个问题，前者是在求模型发挥，后者是在改造环境，彻底决定了 Agent 下次会不会再犯。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776074079332-e965439a-b89c-4c3d-9576-5240dc440343.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以你看，在 Codex 这种 Agent 主导的工程体系里，工程师的价值不再是「我一天能写多少行代码」，而是「我能为 Agent 设计多好的一套运行环境」。这才是未来程序员真正的技术含量所在。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原则三：Agent 反复失败的时候，别问模型能不能更努力，要问环境还缺什么&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;难题四，规范文件越写越长，为啥 Agent 反而更糊涂？&lt;/h3&gt;
&lt;p&gt;这个是 OpenAI 自己亲自踩过的坑，说出来挺打脸的。&lt;/p&gt;
&lt;p&gt;OpenAI 早期做 Codex 的时候，搞了一个特别大的 &lt;a href=&quot;http://AGENTS.md&quot;&gt;AGENTS.md&lt;/a&gt; 文件，把所有的规范、所有的约定、所有的最佳实践全部塞进去。他们当时的想法是：把规则写得越全越好，Agent 就越不会出错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776075029568-13d755ae-5106-43c5-8f32-40b33da971f4.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结果呢？Agent 更糊涂了。&lt;/p&gt;
&lt;p&gt;为啥？因为上下文窗口是稀缺资源。这个文件被当作系统提示词每次都注入进去，当它变得越来越长的时候，模型的注意力被严重稀释。它看到的东西太多，反而抓不住当前任务真正需要的那部分。&lt;/p&gt;
&lt;p&gt;这其实就是我们前面讲过的「上下文腐化」在规则文件这个场景的具体表现。&lt;/p&gt;
&lt;p&gt;OpenAI 后来怎么改的？他们把 &lt;a href=&quot;http://AGENTS.md&quot;&gt;AGENTS.md&lt;/a&gt; 从一本「百科全书」改成了一个「目录页」：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;主文件只保留 100 行左右的核心索引&lt;/strong&gt;。你没看错，OpenAI 原文博客里特地写明了这个数字：整个 &lt;a href=&quot;http://AGENTS.md&quot;&gt;AGENTS.md&lt;/a&gt; 控制在大约 100 行，它不告诉 Agent 每条细节规则，只告诉 Agent「你想看什么，去哪儿看」&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;详细的内容拆到具体的子文档里：架构文档一份、设计原则一份、产品规格一份、执行计划一份、质量评分一份，每份都有清晰的主题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Agent 平时只看目录，只有真的需要某一部分的时候，才钻进对应的子文档&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776077018342-5663ed9a-9f17-4b80-9ea1-9a93ff886a1c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这套做法其实就是我们前面讲的&lt;strong&gt;渐进式披露&lt;/strong&gt;（Progressive Disclosure）。&lt;/p&gt;
&lt;p&gt;我特别喜欢这个思路，因为它和我们做软件设计里的「按需加载」「懒加载」一脉相承：上下文优化的本质，不是「给得越多越好」，而是「该给的时候给，不该给的时候藏起来」。&lt;/p&gt;
&lt;p&gt;这一点其实和我们前面讲的 Agent Skills 是同一回事，前后呼应上了。如果你现在在写 &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt; 或者 Cursor Rules 这种文件，强烈建议你回头看看自己写的有没有「百科全书化」。如果有，赶紧拆。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原则四：规则文件宁缺毋滥，给模型看的东西少即是多&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;难题五，Agent 写的代码越堆越烂，技术债怎么还？&lt;/h3&gt;
&lt;p&gt;前面四个难题都还算比较抽象，这一个特别具体、特别接地气，是 OpenAI 团队在《Harness engineering》博客里亲口承认的一个「我们一开始做错了」的故事。&lt;/p&gt;
&lt;p&gt;先说现象。当 Agent 负责写绝大多数代码之后，会发生一件很自然但也很糟糕的事：&lt;strong&gt;Agent 会疯狂模仿仓库里已有的代码模式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;好的模式会被复制，坏的模式也会被复制。一旦早期某段代码写歪了，Agent 会把那个歪的写法当成「惯例」，越堆越多，越堆越歪，最后整个代码库开始「腐烂」。这个现象 OpenAI 团队给它起了一个很扎心的名字，叫 &lt;strong&gt;AI slop&lt;/strong&gt;，AI 代码泔水。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776077952749-26e2c3d3-48eb-4cdb-9f90-2ce6145f19ed.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OpenAI 团队一开始是怎么解决这个问题的？用最朴素的办法：&lt;strong&gt;靠人工清理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;他们每周拿出整个周五（也就是一周 20% 的时间）让人类工程师去手工打扫 AI slop。你想想这个画面：一群 OpenAI 的高级工程师每周五不干别的，专门给 Agent 擦屁股。&lt;/p&gt;
&lt;p&gt;然后呢？&lt;strong&gt;这个方案失败了&lt;/strong&gt;。原因很直接：Agent 产出代码的速度太快，人类工程师清理的速度根本跟不上，周五清一天，周一一早又堆满了新的。这是一个典型的「人力怎么都追不上机器」的尴尬局面。&lt;/p&gt;
&lt;p&gt;那 OpenAI 最后是怎么改的？他们用一个非常 Harness 的思路解决了这个问题。做法分两步：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776077184668-87e9a406-337d-4d53-affb-3255b1752895.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一步，把人类工程师关于&quot;什么是好代码&quot;的经验，写成一套「Golden Principles」（黄金原则）沉进仓库&lt;/strong&gt;。比如「优先用共享工具包而不是手写 helper」「不要瞎猜数据格式，必须校验边界或用带类型的 SDK」，这些都是有经验的工程师脑子里的隐性知识，以前只存在 code review 的讨论里，现在被显式地写成了规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二步，让一批后台 Agent 按固定节奏自动跑&lt;/strong&gt;。这些 Agent 定期去扫描仓库，对比 Golden Principles，找出偏离的地方，然后自动开修复 PR。大部分修复 PR 可以在 1 分钟内被人类审完，直接 auto merge。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你品品这个做法。它其实把「还技术债」这件事从&lt;strong&gt;一周一次的人工集中清扫&lt;/strong&gt;，变成了&lt;strong&gt;每天持续进行的自动偿还&lt;/strong&gt;。OpenAI 原文里有一句话我特别喜欢：&lt;/p&gt;
&lt;p&gt;技术债就像一笔高利息贷款，几乎永远应该每天小额还一点点，而不是攒着等某一天集中还。&lt;/p&gt;
&lt;p&gt;这太准确了。你想想我们平时写代码是不是也这样？谁都知道技术债要清，但总是说「等这个季度忙完了统一重构」，结果永远也没有「不忙的季度」，技术债越滚越大。Agent 帮人类解决这个老毛病的办法，居然是把经验固化成规则，然后用 Agent 自己去对付 Agent。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原则五：技术债不是攒一堆集中还，而是每天让后台 Agent 自动偿还一点&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;顺便提一个反直觉的发现：Agent 用「老技术」反而更稳&lt;/h3&gt;
&lt;p&gt;讲完五个难题，我想再顺手补一个 OpenAI 博客里特别反直觉的小观察，虽然它不算难题，但值得你记住。&lt;/p&gt;
&lt;p&gt;很多人做 AI 编程的时候有一个想当然的认知：AI 是最前沿的东西，那当然应该配上最前沿的技术栈，什么新框架都给 Agent 上一套。但 OpenAI 在实践中发现，&lt;strong&gt;事情正好反过来&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;Agent 对那些被人类称为「boring」的老技术反而掌握得最好。&lt;/p&gt;
&lt;p&gt;为啥？OpenAI 原文给了三个原因：&lt;strong&gt;组合性好、API 稳定、训练数据里出现得多&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776078107343-4771f24d-9443-4149-93fd-032dce798847.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你想想这个逻辑就通了：AI 训练数据里，一个出现了十几二十年的老库，相关的代码示例、StackOverflow 问答、博客文章多如牛毛，AI 对它的各种用法了如指掌。而一个昨天才出的新框架，AI 只看过零星几篇文档，很容易张冠李戴。&lt;/p&gt;
&lt;p&gt;所以 OpenAI 在 Codex 项目里，甚至会&lt;strong&gt;主动选择那些看起来很土的、甚至业界已经有点嫌弃的技术栈&lt;/strong&gt;，就因为 Agent 能把它们用得更稳。&lt;/p&gt;
&lt;p&gt;有一个特别极端的例子：有时候他们宁愿让 Agent 自己实现一个小工具函数，也不去引入一个流行的 npm 包，因为自己写的代码 Agent 能 100% 理解和控制，而第三方包里藏着 Agent 看不懂的黑盒行为。&lt;/p&gt;
&lt;p&gt;这个发现给你的实际启发是：&lt;strong&gt;如果你要做一个让 Agent 跑得稳的项目，在选技术栈的时候，不要一味追新&lt;/strong&gt;。越老、文档越齐全、在开源社区里沉淀了越久的技术，Agent 反而越容易帮你做对。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;好，五个难题 + 一个反直觉发现，这一章就讲完了。&lt;/p&gt;
&lt;p&gt;你可能已经感觉到一件事：&lt;strong&gt;这些难题的解法全都有一个共同点，它们都不是在调模型，而是在设计模型外面的那一整套环境&lt;/strong&gt;。这就是 Harness Engineering 的灵魂所在。&lt;/p&gt;
&lt;p&gt;我也帮你把这五条原则打包成一个小口诀，带着它进入下一章：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重启胜过修补，生产验收分家，与其催模型不如改环境，规则宁缺毋滥，技术债天天还&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这五条原则贯穿了大厂所有的实践，也回答了「Harness 真正难在哪」。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;写到这儿，我想给你把整篇文章的思路再梳理一遍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776078351887-9d4631e9-2c5f-4669-a1d4-922a8254da47.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;AI 工程的重心，过去两年换过三次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Prompt Engineering 解决的是「怎么把任务讲清楚」。它的核心是塑造模型的概率空间，让模型「听懂」你想干啥。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Context Engineering 解决的是「怎么把信息送对」。它的核心是动态管理大模型的上下文，让模型「知道」该用什么信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Harness Engineering 解决的是「怎么让模型在真实执行中持续做对」。它的核心是设计一整套包裹模型的运行环境，让模型「做对」一连串的事。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三个东西不是替代关系，而是层层包含的关系。&lt;/p&gt;
&lt;p&gt;Prompt 是 Context 的一部分，Context 是 Harness 的一部分。&lt;/p&gt;
&lt;p&gt;当任务还是简单的单轮对话的时候，Prompt 就够用；当任务开始需要外部知识的时候，Context 就关键了；但当模型真正进入「长链路、可执行、低容错」的真实场景，Harness 几乎是不可避免的。&lt;/p&gt;
&lt;p&gt;到了今天这个阶段，整个 AI 圈也越来越清楚一件事：&lt;/p&gt;
&lt;p&gt;AI 落地的核心挑战，正在从「让模型看起来更聪明」，转向「让模型在真实世界里稳定地工作」。&lt;/p&gt;
&lt;p&gt;模型决定了一个 Agent 的天花板，但 Harness 决定了它能不能落地、能不能稳定交付、能不能真正跑在生产环境里。这就是为啥同样的模型，在不同的产品里效果差距会这么大。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1776079105770-eb411cb5-e6f7-4c7c-846d-ceeb23555ac1.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你最近也在做 Agent 相关的事情，我特别建议你：别再把所有精力都花在调模型、调提示词上了。&lt;/p&gt;
&lt;p&gt;回过头来看看你的 Harness 长啥样，看看你有没有规则文件、有没有校验闭环、有没有任务编排、有没有评估机制、有没有失败恢复。这些东西，每一项都能让你的 Agent 上一个台阶。&lt;/p&gt;
&lt;p&gt;顺便说一个值得你深思的趋势。如果你真的把 Harness 这件事认真做几个月，你会发现一个很微妙的变化：你花在「亲手写代码」上的时间越来越少，花在「写规则、写流程、设计环境」上的时间越来越多。&lt;/p&gt;
&lt;p&gt;OpenAI Codex 团队那几个工程师能撬动百万行代码，靠的就不是键盘敲得快，而是把 Agent 的运行环境设计到位。&lt;/p&gt;
&lt;p&gt;写代码这件事本身会越来越多地交给 Agent，但设计一个能让 Agent 高质量产出的环境，这件事在很长一段时间里都得是人来做。这可能就是未来程序员真正的主战场。&lt;/p&gt;
&lt;p&gt;最后，再用开头那个在圈子里被广泛引用的等式给你收个尾：&lt;/p&gt;
&lt;p&gt;Agent = Model + Harness&lt;/p&gt;
&lt;p&gt;你的 Agent 想变得更好，要么换更强的模型，要么写更好的 Harness。在模型迭代速度逐渐放缓的今天，Harness 这部分的提升空间，可能比你想象的大得多。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考资料&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mitchell Hashimoto&lt;/strong&gt;，《My AI Adoption Journey》, &lt;a href=&quot;https://mitchellh.com/writing/my-ai-adoption-journey&quot;&gt;https://mitchellh.com/writing/my-ai-adoption-journey&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OpenAI&lt;/strong&gt;，《Harness engineering: leveraging Codex in an agent-first world, &lt;a href=&quot;https://openai.com/index/harness-engineering/&quot;&gt;https://openai.com/index/harness-engineering/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Anthropic&lt;/strong&gt;，《Effective harnesses for long-running agents》, &lt;a href=&quot;https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents&quot;&gt;https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Anthropic&lt;/strong&gt;，《Harness design for long-running application development》, &lt;a href=&quot;https://www.anthropic.com/engineering/harness-design-long-running-apps&quot;&gt;https://www.anthropic.com/engineering/harness-design-long-running-apps&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Anthropic&lt;/strong&gt;，《Effective context engineering for AI agents》, &lt;a href=&quot;https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents&quot;&gt;https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LangChain&lt;/strong&gt;，《Improving Deep Agents with harness engineering》, &lt;a href=&quot;https://blog.langchain.com/improving-deep-agents-with-harness-engineering/&quot;&gt;https://blog.langchain.com/improving-deep-agents-with-harness-engineering/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cognition&lt;/strong&gt;，《Rebuilding Devin for Claude Sonnet 4.5: Lessons and Challenges》, &lt;a href=&quot;https://cognition.ai/blog/devin-sonnet-4-5-lessons-and-challenges&quot;&gt;https://cognition.ai/blog/devin-sonnet-4-5-lessons-and-challenges&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/engineering/harness-engineering.html&quot;&gt;agent/engineering/harness-engineering.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🤖AI Agent 是什么？Agent 面试题万字图解</title><link>https://www.corgi37.top/posts/ai-agent-shi-shenme-mianshiti-wanzi-tujie/</link><guid isPermaLink="true">https://www.corgi37.top/posts/ai-agent-shi-shenme-mianshiti-wanzi-tujie/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今年，AI 圈子里最火的一个词，毫无疑问就是 &lt;strong&gt;Agent&lt;/strong&gt;（智能体）。&lt;/p&gt;
&lt;p&gt;打开技术社区，满屏都是 Agent，各大厂商发布会，也都在讲 Agent。&lt;/p&gt;
&lt;p&gt;今天这篇文章，我就用最通俗易懂的方式，把 Agent 相关的核心概念一次性给你讲清楚。这些也是面试中经常会被问到的问题，搞懂了不仅能应付面试，更能帮你真正理解 AI 技术的主旋律。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;LLM 和 Agent 有什么区别？&lt;/h2&gt;
&lt;p&gt;要搞懂 Agent，咱们得先从 LLM 聊起，因为 Agent 本质上就是在 LLM 的基础上进化出来的。&lt;/p&gt;
&lt;h3&gt;什么是 LLM？&lt;/h3&gt;
&lt;p&gt;LLM，全称 Large Language Model，翻译过来就是&lt;strong&gt;大语言模型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你可以把它想象成一个读了互联网上几乎所有文字的超级学霸。它通过学习海量的文本数据，掌握了人类语言的各种规律和知识。我们平时用的 ChatGPT、Claude、DeepSeek、文心一言，底层都是大语言模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774708684954-0e0007db-c906-4912-8496-5d6c8eb65cf6.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;LLM 的工作原理说白了就是「预测下一个字」。你给它一段话，它会根据学到的语言规律，一个字一个字地往后接。&lt;/p&gt;
&lt;p&gt;听起来简单，但因为它学的数据量实在太大了，这种「接龙」的效果好到令人吃惊，它能写文章、写代码、做翻译、回答各种专业问题。&lt;/p&gt;
&lt;h3&gt;LLM 有什么弊端？&lt;/h3&gt;
&lt;p&gt;虽然 LLM 非常聪明，但你仔细想想会发现，它其实有点像一个&quot;有嘴没手&quot;的顾问。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774709653498-f1c169a3-e130-4b5f-baff-0fde8341f6da.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一个弊端是&lt;strong&gt;只会「说」不会「做」&lt;/strong&gt;。你让 LLM「帮我订一张机票」，它会详细告诉你怎么订，但它真没法替你去携程下单。你让它「帮我把这个 Bug 修了」，它能给你改好的代码，但它没法自己打开编辑器去改文件、跑测试。说白了，LLM 的能力被困在对话框里了，它没法跟外部世界互动，没法操作任何系统。&lt;/p&gt;
&lt;p&gt;第二个弊端是&lt;strong&gt;没有「记忆」&lt;/strong&gt;。你跟 ChatGPT 聊了一下午，聊了很多你的个人情况和项目背景。结果第二天开一个新对话，它完全不记得你是谁了。因为 LLM 的记忆只限于当前这轮对话的上下文窗口，对话一结束，一切归零。&lt;/p&gt;
&lt;p&gt;第三个弊端是&lt;strong&gt;不会用「工具」&lt;/strong&gt;。你问 LLM 今天上海天气怎么样，它只能根据训练数据里的旧知识来猜，而不是像你一样打开天气 App 查实时数据。LLM 本身不能上网搜索、不能查数据库、不能调 API，所有回答都来自它「脑子里」已有的知识，而这些知识不仅有截止日期，还可能是错的，也就是常说的「幻觉」问题。&lt;/p&gt;
&lt;p&gt;第四个弊端是&lt;strong&gt;不会「规划」&lt;/strong&gt;。如果你给 LLM 一个复杂任务，比如「帮我做一份竞品分析报告」，它只能一次性生成一大段文字。它不会像人一样先想想应该先搜集哪些信息、分析哪些维度、用什么框架来组织，然后一步一步去执行。LLM 是「被动响应型」的，你问一句它答一句，没法自主拆解任务、制定计划、分步执行。&lt;/p&gt;
&lt;h3&gt;那 Agent 是什么？&lt;/h3&gt;
&lt;p&gt;讲完弊端，你可能已经在想：有没有一种方式，能让 LLM 不仅会「说」，还能「做」呢？这就是 Agent 要解决的问题。&lt;/p&gt;
&lt;p&gt;Agent，翻译过来叫&lt;strong&gt;智能体&lt;/strong&gt;。简单来说：&lt;strong&gt;Agent 就是 LLM 在循环中自主使用工具的系统。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774709529267-90ce12a1-a233-4673-88b5-b5ce1edc80ec.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这句话有三个关键词，「LLM」说明 Agent 的核心大脑还是大模型，「工具」说明它能调用外部能力，「循环」说明它不是一问一答就结束，而是会不断地思考、行动、观察结果、再思考，直到任务完成。&lt;/p&gt;
&lt;p&gt;打个比方，如果 LLM 是一个只会给你建议的顾问，你问他「怎么装修房子」，他能讲一大堆方案，但绝不会亲自动手。&lt;/p&gt;
&lt;p&gt;那 Agent 就是一个能动手干活的项目经理，你说「帮我把房子装修好」，他会自己去找装修队、买材料、盯进度、解决问题，直到装修完成。&lt;/p&gt;
&lt;h3&gt;Agent 怎么解决 LLM 的弊端？&lt;/h3&gt;
&lt;p&gt;其实理解了上面的比喻，答案就很清楚了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774709384301-e60719af-012c-45f4-93dc-138cedc22b32.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;针对&quot;只会说不会做&quot;，Agent 引入了&lt;strong&gt;工具调用&lt;/strong&gt;能力，可以调用搜索引擎、数据库、API、代码执行器等各种外部工具来真正执行操作。&lt;/li&gt;
&lt;li&gt;针对&quot;没有记忆&quot;，Agent 配备了&lt;strong&gt;记忆系统&lt;/strong&gt;，包括记住当前任务上下文的短期记忆，和存储在外部数据库中、可以跨对话保留的长期记忆。&lt;/li&gt;
&lt;li&gt;针对&quot;不会用工具&quot;，业界推出了 MCP 等&lt;strong&gt;标准化协议&lt;/strong&gt;来统一工具接入方式，后面会详细讲。&lt;/li&gt;
&lt;li&gt;针对&quot;不会规划&quot;，Agent 具备了&lt;strong&gt;任务拆解和规划&lt;/strong&gt;能力，能把一个大目标分解成多个小步骤，然后逐步执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Agent 的核心组成&lt;/h3&gt;
&lt;p&gt;所以一个完整的 Agent 其实就是四个模块的组合。&lt;/p&gt;
&lt;p&gt;第一个是&lt;strong&gt;大脑&lt;/strong&gt;，也就是 LLM，负责理解意图、推理判断、决定下一步行动。第二个是&lt;strong&gt;规划模块&lt;/strong&gt;，负责把复杂任务拆解成可执行的步骤。第三个是&lt;strong&gt;记忆模块&lt;/strong&gt;，负责存储和检索信息，让 Agent 能在长时间任务中保持连贯。第四个是&lt;strong&gt;工具模块&lt;/strong&gt;，是 Agent 的「手和脚」，让它能跟外部世界互动。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774709785444-2b178b49-8895-4006-9667-1cc0a9014932.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用一个实际例子来感受。假设你对 Agent 说：「帮我查一下下周三上海的天气，如果不下雨就在日历上安排一个户外团建。」如果是 LLM，它只会告诉你「你可以通过天气 App 查看天气，然后在日历上创建事件」。&lt;/p&gt;
&lt;p&gt;而 Agent 会直接调用天气 API 查到下周三多云 25°C 无降雨，然后自动调用日历 API 创建团建事件，最后告诉你「已安排好了」。LLM 告诉你「怎么做」，Agent 直接帮你「做完了」。这就是本质区别。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Agent 和 Workflow 有什么区别？&lt;/h2&gt;
&lt;p&gt;搞懂了 LLM 和 Agent 的区别之后，你可能还会碰到另一个容易搞混的概念，&lt;strong&gt;Workflow（工作流）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;很多人把 Agent 和 Workflow 混为一谈，但它们的设计理念其实完全不同。&lt;/p&gt;
&lt;h3&gt;先用一个场景来感受区别&lt;/h3&gt;
&lt;p&gt;假设有一个任务：处理客户的退款申请。Workflow 的做法是这样的，开发者提前写好整个流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 第一步接收申请
- 第二步调用 LLM 提取关键信息
- 第三步查数据库获取订单详情
- 第四步调用 LLM 判断是否符合退款政策
- 第五步执行退款或生成拒绝邮件，第六步发送通知。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一步做什么、接下来走哪条路，全都是提前在代码里写死的，LLM 只是在某些步骤中被召唤出来做理解和判断。&lt;/p&gt;
&lt;p&gt;而 Agent 的做法完全不同。它收到「处理这个退款申请」的任务后，自己来决定怎么做，先看看申请写了什么，然后觉得需要查一下订单信息，发现情况有点特殊就去搜索退款政策文档，推理判断后决定执行退款，最后给客户发邮件通知。&lt;/p&gt;
&lt;p&gt;整个过程中，每一步做什么都是 Agent 自己决定的，而不是代码预先规定的。&lt;/p&gt;
&lt;h3&gt;两者的定义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Workflow 是指 LLM 和工具通过预定义的代码路径进行编排的系统&lt;/strong&gt;，而 &lt;strong&gt;Agent 是指 LLM 动态主导自身流程与工具调用的系统，由 LLM 自主决定如何完成任务&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774710003815-3904e095-6cd1-4537-9d82-db1275632dfb.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;翻译成大白话就是：Workflow 是「我（开发者）告诉你每一步该做什么」，Agent 是「我告诉你目标，你自己决定怎么做」。&lt;/p&gt;
&lt;p&gt;你可以这样类比：Workflow 就像一条工厂流水线，每个工位做什么、零件从哪来到哪去，全都是提前设计好的。工人（LLM）只需要在自己的工位上完成指定动作。&lt;/p&gt;
&lt;p&gt;而 Agent 更像一个自主工作的项目经理，老板只告诉他&quot;把这件事搞定&quot;，然后他自己去调研、制定计划、协调资源、推进执行。&lt;/p&gt;
&lt;h3&gt;核心区别在哪？&lt;/h3&gt;
&lt;p&gt;两者最核心的区别在于**「谁在控制流程」**。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774742710947-fca6fd37-dbc5-4ffa-8e68-3004bc69ee23.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Workflow 的控制权在代码手里，流程是确定的、可预测的、可复现的，但灵活性比较差。Agent 的控制权在 LLM 手里，行为是动态的、灵活的、能适应变化的，但相应地也带来了不确定性。&lt;/p&gt;
&lt;p&gt;从成本角度看，Workflow 因为流程固定，token 消耗比较省，大约是 Agent 的四分之一。Agent 因为需要反复推理决策，token 消耗要高得多。&lt;/p&gt;
&lt;p&gt;从可靠性看，Workflow 行为可预测，出了问题容易定位；Agent 决策路径不确定，调试起来更困难。&lt;/p&gt;
&lt;h3&gt;什么时候用 Workflow，什么时候用 Agent？&lt;/h3&gt;
&lt;p&gt;Anthropic 给了一个非常实用的建议：&lt;strong&gt;从最简单的方案开始，只在明确需要时才增加复杂度。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果任务步骤是固定的、可以提前规划好的，或者对可靠性要求很高（比如金融交易、医疗系统），那就用 Workflow。如果任务是开放式的、无法预知所有步骤，或者需要灵活应对各种意外情况，那就用 Agent。&lt;/p&gt;
&lt;p&gt;不过在实际生产环境中，最常见的其实是&lt;strong&gt;混合架构&lt;/strong&gt;，Workflow 和 Agent 的结合。正如 LangChain 说的：&quot;大多数生产中的 Agent 系统其实是 Workflow 和 Agent 的组合。&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774710613959-49799ddd-7a0b-4918-932f-c172433743e1.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如一个智能客服系统，整体流程用 Workflow 控制（接收工单→分类→处理→回复），但在&quot;处理&quot;环节遇到复杂问题时，会启动一个 Agent 来自主分析和解决。&lt;/p&gt;
&lt;p&gt;所以不要把两者对立起来，它们更像是工具箱里的锤子和螺丝刀，不是竞争关系，而是配合关系。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Agent 有什么工作模式？&lt;/h2&gt;
&lt;p&gt;了解了 Agent 是什么之后，下一个问题就是：Agent 到底是怎么「干活」的？就像人干活有不同的方式，有人喜欢边做边想，有人喜欢先列计划再动手，有人喜欢团队协作，Agent 干活也有不同的工作模式。&lt;/p&gt;
&lt;h3&gt;模式一：ReAct（边想边做）&lt;/h3&gt;
&lt;p&gt;ReAct 是目前最经典、最基础的 Agent 工作模式，名字来源于 Reasoning + Acting 的缩写，也就是「推理 + 行动」。几乎所有主流的 Agent 框架底层都在用它。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774710960583-ab96e5fd-4e25-420a-a7c7-d1f7197f8a67.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它的核心思想非常简单：Agent 在思考和行动之间不断交替。具体来说就是一个三步循环，先是&lt;strong&gt;思考（Thought）&lt;/strong&gt;，分析当前情况决定下一步做什么；然后是&lt;strong&gt;行动（Action）&lt;/strong&gt;，调用一个工具来执行；接着是&lt;strong&gt;观察（Observation）&lt;/strong&gt;，查看工具返回的结果。然后回到思考，如此循环，直到任务完成。&lt;/p&gt;
&lt;p&gt;打个生活化的比方。想象你要收拾行李准备出差，你会先想「我要去上海三天，先看看天气怎么样」，然后打开手机查天气预报，发现会下雨气温 15-20°C，接着想「得带伞和外套」，于是去找出来放进行李箱，再想「还得确认酒店」，打开 App 检查预订信息... 这就是 ReAct 的精髓，每一步都先想后做，做完看结果，再决定下一步。&lt;/p&gt;
&lt;p&gt;ReAct 的优点是透明可审计（每一步思考过程都看得见）、灵活适应（遇到意外能随时调整）、通用性强。但它的缺点也很明显：token 消耗大，因为每一步都要完整推理一次；有时会陷入循环，反复执行相同动作走不出来。&lt;/p&gt;
&lt;h3&gt;模式二：Plan-and-Execute（先想好再做）&lt;/h3&gt;
&lt;p&gt;如果说 ReAct 是「边想边做」，Plan-and-Execute 就是「先想好再做」。&lt;/p&gt;
&lt;p&gt;它把 Agent 的工作分成两个阶段：第一阶段是规划，Agent 先把完整的执行计划想清楚；第二阶段是执行，按计划逐步完成，不用每步都重新思考全局。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774711188559-2e5c6e3d-89ce-4106-99bb-2ada5c783daf.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用出差的例子来对比，ReAct 是「查天气→想想带什么→找衣服→想想还缺什么→查酒店→想想还要准备什么...」，每一步都重新审视全局。而 Plan-and-Execute 是先列一个清单（查天气、准备衣物、确认酒店、准备证件、叫车），然后逐项打勾执行。&lt;/p&gt;
&lt;p&gt;这种模式最大的优势是&lt;strong&gt;省钱&lt;/strong&gt;。规划只做一次，执行阶段不用反复推理，token 消耗大约是 ReAct 的五分之一。&lt;/p&gt;
&lt;p&gt;但缺点是不够灵活，如果执行到第 3 步发现情况变了，原来的计划可能就不适用了。所以也有这么个做法是在执行过程中加入「重新规划检查点」，每隔几步检查一下计划是否还靠谱。&lt;/p&gt;
&lt;h3&gt;模式三：Reflection（做完再检查）&lt;/h3&gt;
&lt;p&gt;反思模式的核心思想是 Agent 完成任务后不急着交付，而是先自我检查一遍，就像你写完文章不会直接发出去，而是再通读一遍、改一改、润色一下。&lt;/p&gt;
&lt;p&gt;实现方式通常有两种。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774711350482-88afc8ba-2904-4a98-8633-e575eee194cb.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一种是&lt;strong&gt;自我反思&lt;/strong&gt;，同一个 Agent 完成任务后切换到「审查者」角色来审视自己的输出，发现问题就修改，然后再审查，直到满意。&lt;/p&gt;
&lt;p&gt;另一种是&lt;strong&gt;双 Agent 对话&lt;/strong&gt;，一个 Agent 负责生成，另一个负责评审，两者来回迭代直到评审方满意，就像代码的 Code Review 过程。这种模式特别适合对质量要求高的场景，比如代码生成、法律文书、学术论文等。&lt;/p&gt;
&lt;h3&gt;模式四：Multi-Agent（团队协作）&lt;/h3&gt;
&lt;p&gt;当任务太复杂、一个 Agent 搞不定的时候怎么办？&lt;/p&gt;
&lt;p&gt;答案是派一个团队上。Multi-Agent 模式让多个专业化的 Agent 各司其职，比如一个负责规划、一个负责搜集信息、一个负责写代码、一个负责测试，通过协作来完成复杂任务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774711630472-8bab800b-9cf3-43a4-977d-a70bc40a1127.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这就像一个项目团队，有产品经理、研究员、开发者、测试员，各自做自己擅长的事。目前主流的多 Agent 框架包括 LangGraph、CrewAI、OpenAI Agents SDK 和微软的 AutoGen 等。&lt;/p&gt;
&lt;p&gt;不过 Anthropic 提醒过：&lt;strong&gt;不要过早引入多 Agent 架构&lt;/strong&gt;。很多时候，一个强大的单 Agent 就够用了。只有任务确实需要拆分成多个并行子任务时，多 Agent 才值得引入。&lt;/p&gt;
&lt;p&gt;最后总结一下：这几种模式不是互斥的，实际中往往是组合使用的。一个多 Agent 系统中，每个 Agent 内部可能用的是 ReAct 模式，整体协作用的是 Multi-Agent 模式，最后还有一个 Reflection 环节来检查质量。&lt;/p&gt;
&lt;p&gt;选择哪种模式，关键看你的任务特点和对灵活性、成本、质量的优先级排序。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Function Call 是什么？&lt;/h2&gt;
&lt;p&gt;前面我们聊 Agent 的时候反复提到一个词，「工具调用」。Agent 能查天气、能搜索信息、能操作数据库，这些能力是怎么实现的？&lt;/p&gt;
&lt;p&gt;答案就是 &lt;strong&gt;Function Call（函数调用）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;从「只会说话」到「能做事情」&lt;/h3&gt;
&lt;p&gt;2023 年之前，大语言模型只能做一件事：生成文本。你问它问题，它给你一段文字回答，仅此而已。它说的再好听，也只是「说」，不能「做」。&lt;/p&gt;
&lt;p&gt;Function Call 的出现彻底改变了这个局面。它是 OpenAI 在 2023 年 6 月率先推出的一种能力，简单来说就是&lt;strong&gt;让 LLM 不仅能生成文字，还能告诉外部程序「我想调用某个函数，参数是这些」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774742609464-b6c7aee4-75c2-4608-9c9e-b1e920d3432e.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打个比方。在没有 Function Call 之前，LLM 就像一个只能写字的人，你问他天气，他只能根据记忆回答「上海通常三月份比较潮湿」。&lt;/p&gt;
&lt;p&gt;有了 Function Call 之后，这个人学会了「打电话」，你问他天气，他会拿起电话拨给天气台（调用天气 API），听到对方报的实时数据后再告诉你「今天上海 22°C，多云」。&lt;/p&gt;
&lt;h3&gt;Function Call 的工作原理&lt;/h3&gt;
&lt;p&gt;Function Call 的工作流程分四步。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774711968337-903e9a23-0d30-43b1-bb55-731cca9da18a.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一步，&lt;strong&gt;定义函数&lt;/strong&gt;。开发者预先告诉 LLM「你手边有哪些工具可以用」，用 JSON 格式描述每个函数的名字、功能说明和参数。比如你告诉它有一个 &lt;code&gt;get_weather&lt;/code&gt; 函数，接收一个城市名参数，返回天气信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;tools&quot;: [
    {
      &quot;type&quot;: &quot;function&quot;,
      &quot;function&quot;: {
        &quot;name&quot;: &quot;get_weather&quot;,
        &quot;description&quot;: &quot;获取指定城市的实时天气&quot;,
        &quot;parameters&quot;: {
          &quot;type&quot;: &quot;object&quot;,
          &quot;properties&quot;: {
            &quot;city&quot;: {
              &quot;type&quot;: &quot;string&quot;, 
              &quot;description&quot;: &quot;城市名称，比如：上海&quot;
            }
          },
          &quot;required&quot;: [&quot;city&quot;]
        }
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步，&lt;strong&gt;模型判断&lt;/strong&gt;。用户提问后，LLM 分析用户的意图，自己判断「要回答这个问题，我需要调用哪个函数」。如果用户问「上海今天天气如何」，LLM 会决定调用 &lt;code&gt;get_weather&lt;/code&gt;，并生成参数 &lt;code&gt;{&quot;city&quot;: &quot;上海&quot;}&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;tool_calls&quot;: [
    {
      &quot;type&quot;: &quot;function&quot;,
      &quot;function&quot;: {
        &quot;name&quot;: &quot;get_weather&quot;,
        &quot;arguments&quot;: &quot;{\&quot;city\&quot;: \&quot;上海\&quot;}&quot;
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三步，&lt;strong&gt;执行函数&lt;/strong&gt;。注意，这一步非常关键，&lt;strong&gt;LLM 自己并不执行函数&lt;/strong&gt;。它只是输出了「我想调用这个函数，参数是这些」的结构化指令。真正执行函数的是你的应用程序。你的代码拿到 LLM 返回的调用指令后，解析出 &lt;code&gt;city=上海&lt;/code&gt;，去实际调用天气 API，拿到结果比如 &lt;code&gt;22度，多云&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第四步，&lt;strong&gt;生成回答&lt;/strong&gt;。你的代码把拿到的真实温度数据再次发给 LLM。LLM 这次有了客观数据支撑，就会用非常自然的人类语言回复你：今天上海天气是多云，气温大约 22 摄氏度。&lt;/p&gt;
&lt;h3&gt;为什么 Function Call 这么重要？&lt;/h3&gt;
&lt;p&gt;你可能会觉得，这不就是「让 LLM 调 API」吗？有什么了不起的？&lt;/p&gt;
&lt;p&gt;关键在于，Function Call 解决了两个核心问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个是**&quot;什么时候调用&quot;的判断问题**，LLM 能根据用户的自然语言意图，自动判断需不需要调用工具、调用哪个工具。你不需要写复杂的条件判断逻辑，LLM 自己会推理。&lt;/li&gt;
&lt;li&gt;第二个是**&quot;传什么参数&quot;的提取问题**，LLM 能从用户的自然语言中提取出结构化的参数。用户说&quot;帮我查一下北京后天的天气&quot;，LLM 能自动提取出 &lt;code&gt;city=北京&lt;/code&gt; 和 &lt;code&gt;date=后天&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个能力加在一起，就把 LLM 从一个「只会聊天的文本生成器」变成了一个「能理解意图并驱动外部系统的决策引擎」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774715038535-208195cb-caeb-4fa9-8542-18836901f621.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而这正是 Agent 的基石。可以说，&lt;strong&gt;Function Call 就是 Agent 能力的最底层技术基础&lt;/strong&gt;，没有 Function Call，Agent 就无法调用工具，也就没法真正「做事」。&lt;/p&gt;
&lt;p&gt;目前几乎所有主流大模型都支持 Function Call，包括 OpenAI 的 GPT 系列、Anthropic 的 Claude 系列、Google 的 Gemini 系列，以及各种开源模型如 Llama 等。虽然各家的 API 格式略有不同，但核心原理是一样的。&lt;/p&gt;
&lt;h3&gt;Function Call 和 Agent 的关系&lt;/h3&gt;
&lt;p&gt;最后说一下两者的关系。&lt;/p&gt;
&lt;p&gt;Function Call 是一次性的「单步调用」，LLM 判断需要调用一个函数，调用完就结束了。而 Agent 是「循环调用」，Agent 在一个循环中反复使用 Function Call，每次调用后观察结果，再决定下一步要不要继续调用其他函数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774742943390-7e63c4af-be30-4d28-ad2c-a4d3734f59ab.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以 Function Call 是 Agent 的「原子操作」，Agent 是 Function Call 的「高级编排」。一个 Agent 完成一个复杂任务，可能需要连续进行十几次 Function Call。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;MCP 是什么协议？&lt;/h2&gt;
&lt;p&gt;前面讲了 Function Call 让 LLM 能调用工具。但随着 Agent 越来越强大，需要连接的工具和服务越来越多，一个新问题浮出水面了，&lt;strong&gt;集成太麻烦了&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Function Call 的集成困境&lt;/h3&gt;
&lt;p&gt;想象一下，你开发了一个 Agent，需要它能连 Slack 发消息、查 Google Drive 的文档、读 GitHub 的代码、查 Postgres 数据库。&lt;/p&gt;
&lt;p&gt;用 Function Call 的方式，你需要为每一个服务单独写适配代码，为 Slack 写一套函数定义和调用逻辑、为 Google Drive 写一套、为 GitHub 写一套、为数据库又写一套。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774713843276-a14ea79f-453c-44bd-8855-2d79ab9f9101.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你有 N 个 AI 应用，要对接 M 个外部服务，就需要写 N × M 个定制集成。这在实际中完全不可扩展。更头疼的是，每个 LLM 厂商的 Function Call 格式还不完全一样，OpenAI 用 &lt;code&gt;tool_calls&lt;/code&gt;，Anthropic 用 &lt;code&gt;tool_use&lt;/code&gt; content block，参数结构也有差异。&lt;/p&gt;
&lt;h3&gt;MCP 的诞生&lt;/h3&gt;
&lt;p&gt;为了解决这个问题，Anthropic 在 2024 年 11 月开源了 &lt;strong&gt;MCP（Model Context Protocol，模型上下文协议）&lt;/strong&gt;。你可以把 MCP 理解为**「AI 界的 USB-C 接口」**。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774712256948-6fe99407-d54d-4e80-9bc3-0d6faed1dd96.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以前，不同的手机、电脑、设备各自用不同的充电线和接口，非常混乱。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774712119395-acf8492e-ec6e-4378-8c37-47c2a4d73b7f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;USB-C 统一了这一切，一根线就能充电、传数据、接显示器。MCP 做的是同样的事情：它提供了一个&lt;strong&gt;统一的标准&lt;/strong&gt;，让任何 AI 应用都能用同一种方式连接任何外部工具和数据源。&lt;/p&gt;
&lt;h3&gt;MCP 是怎么工作的？&lt;/h3&gt;
&lt;p&gt;MCP 的架构很清晰，主要有三个角色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774713035671-f48e04a7-5d12-45fc-87cf-fa700ba4644c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先是 &lt;strong&gt;MCP Host（宿主）&lt;/strong&gt;，就是你使用的 AI 应用，比如 Claude Desktop、Cursor 编辑器、你自己开发的 Agent 应用。它是整个交互的发起方。&lt;/li&gt;
&lt;li&gt;然后是 &lt;strong&gt;MCP Client（客户端）&lt;/strong&gt;，它住在 Host 里面，负责跟 MCP Server 通信。你可以把它理解为&quot;翻译官&quot;，Host 想要什么能力，Client 就去跟对应的 Server 沟通。&lt;/li&gt;
&lt;li&gt;最后是 &lt;strong&gt;MCP Server（服务端）&lt;/strong&gt;，它负责对外暴露具体的工具能力和数据资源。比如有一个 GitHub MCP Server，它能提供&quot;搜索代码&quot;&quot;创建 Issue&quot;&quot;查看 PR&quot;等工具。一个 Slack MCP Server 能提供&quot;发送消息&quot;&quot;搜索频道&quot;等工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个流程就是：用户在 AI 应用中提问 → AI 应用（Host）通过 MCP Client 发现有哪些可用工具 → AI 决定调用某个工具 → MCP Client 向对应的 MCP Server 发送请求 → Server 执行操作返回结果 → AI 基于结果生成回答。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774713721299-1f7a4b9e-ec67-4e33-a93b-8cd126eea598.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;MCP 解决了什么问题？&lt;/h3&gt;
&lt;p&gt;最核心的就是把 N × M 的集成问题变成了 N + M 的问题。&lt;/p&gt;
&lt;p&gt;以前每个 AI 应用要跟每个服务单独对接，现在每个 AI 应用只要支持 MCP 协议（实现一次 Client），每个服务只要提供一个 MCP Server（实现一次 Server），双方就能自动对接。&lt;/p&gt;
&lt;p&gt;新增一个服务不需要改任何 AI 应用的代码，新增一个 AI 应用也不需要改任何服务的代码。&lt;/p&gt;
&lt;p&gt;而且 MCP Server 暴露的工具是&lt;strong&gt;可发现的&lt;/strong&gt;，AI 应用启动时能自动查询有哪些 MCP Server 可用、每个 Server 提供哪些工具、每个工具的参数是什么。&lt;/p&gt;
&lt;p&gt;这意味着 Agent 可以在运行时动态发现新的能力，而不是只能用开发者写死的那些函数。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Skills 是什么？&lt;/h2&gt;
&lt;p&gt;前面讲了 Function Call 让 Agent 能调用函数，MCP 让 Agent 用统一标准连接工具。但你有没有想过一个问题：&lt;strong&gt;Agent 知道怎么调用工具了，但它知道在什么场景下该用什么方法来解决问题吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;打个比方。你给一个新来的实习生一把锤子、一把螺丝刀、一个扳手（这些是工具），但他可能还是不知道&quot;修一把椅子应该先拧螺丝还是先敲钉子、用什么顺序和方法&quot;。他缺的不是工具，而是&lt;strong&gt;经验和方法论&lt;/strong&gt;，也就是&quot;怎么做&quot;的知识。&lt;/p&gt;
&lt;p&gt;这就是 &lt;strong&gt;Skills（技能）&lt;/strong&gt; 要解决的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774712216530-9ceb9ee8-2341-4c3b-b702-38f0eb7b7c8a.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Skills 是什么？&lt;/h3&gt;
&lt;p&gt;Skills 是一种&lt;strong&gt;自然语言指令文件&lt;/strong&gt;，通常是 Markdown 格式，用来教 Agent&quot;在什么场景下、按照什么方法、遵循什么规范来完成特定任务&quot;。&lt;/p&gt;
&lt;p&gt;在 Claude Code、Cursor 等 AI 工具中，Skills 通常以 &lt;code&gt;SKILL.md&lt;/code&gt; 文件的形式存在。&lt;/p&gt;
&lt;p&gt;Skills 的结构很简单：顶部有一段 YAML 格式的元数据，声明这个 Skill 什么时候应该被激活（比如&quot;当用户要求代码审查时&quot;）；下面是具体的行为指令，用自然语言写成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
name: Code_Review_Expert
description: 当用户要求进行代码审查时，自动触发此技能。
triggers:
  - &quot;帮我 review 一下这段代码&quot;
  - &quot;代码审查&quot;
---

# 身份设定
你是一个拥有 10 年开发经验的资深后端架构师，你极其看重代码的可读性、性能和安全性。

# 审查工作流
当你进行代码审查时，你必须严格按照以下步骤进行排查：
1. 看结构：检查代码是否符合单一职责原则，有没有超过 100 行的超长方法。
2. 查漏洞：重点检查是否存在 SQL 注入风险、越权访问风险或空指针异常风险。
3. 审性能：是否有在 for 循环里查数据库的愚蠢操作？是否有流对象没有及时 close 释放？
4. 给方案：你绝对不能只挑毛病，必须针对每个问题给出具体的修改建议，并且附带优化后的代码片段。

# 输出规范
语气要专业、极其直接，不要说废话。直接输出一份 Markdown 格式的审查报告，分点列出问题和修改方案。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;打个更直观的比方&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果说 MCP 给了 Agent 一个装满工具的&lt;strong&gt;厨房&lt;/strong&gt;，有刀、有锅、有烤箱、有各种调料。&lt;/p&gt;
&lt;p&gt;那 Skills 就是一本&lt;strong&gt;菜谱&lt;/strong&gt;，告诉 Agent&quot;做红烧肉要先焯水再炒糖色，加水炖 40 分钟，火候要先大后小&quot;。&lt;/p&gt;
&lt;p&gt;厨房（MCP）解决的是&quot;能做什么&quot;的问题，菜谱（Skills）解决的是&quot;该怎么做&quot;的问题。一个完整的 Agent 两者都需要。&lt;/p&gt;
&lt;h3&gt;Skills 的工作方式&lt;/h3&gt;
&lt;p&gt;Skills 的工作方式跟 Function Call 和 MCP 有一个关键不同：&lt;strong&gt;Skills 的核心是&quot;指令和知识&quot;，但它在执行过程中完全可以调用工具&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体来说，当 Agent 启动时，它会扫描可用的 Skills 列表。当用户提出请求时，Agent 判断有没有匹配的 Skill。如果有，Agent 就把这个 Skill 的内容加载到上下文中，然后按照 Skill 中的指令来思考和行动。&lt;/p&gt;
&lt;p&gt;但这里有一个很重要的点——&lt;strong&gt;Skill 不只是告诉 Agent 怎么想，它还能指导 Agent 怎么做&lt;/strong&gt;。一个 Skill 可以在 &lt;a href=&quot;http://SKILL.md&quot;&gt;SKILL.md&lt;/a&gt; 文件中通过 &lt;code&gt;allowed-tools&lt;/code&gt; 字段声明它需要使用哪些工具（比如 Bash、Read、Edit），也可以打包可执行的脚本文件，甚至可以指导 Agent 去调用 MCP 工具或发起 Function Call。&lt;/p&gt;
&lt;p&gt;举个例子，一个&quot;部署到测试环境&quot;的 Skill，它的 &lt;a href=&quot;http://SKILL.md&quot;&gt;SKILL.md&lt;/a&gt; 不仅包含部署的步骤说明，还会声明 &lt;code&gt;allowed-tools: Bash&lt;/code&gt;，然后在指令中写明&quot;先执行 &lt;code&gt;npm test&lt;/code&gt;，再执行 &lt;code&gt;npm run build&lt;/code&gt;，最后执行部署脚本&quot;。Agent 加载这个 Skill 后，会按照指令一步步调用 Bash 工具来真正执行这些操作。&lt;/p&gt;
&lt;p&gt;所以更准确地说：&lt;strong&gt;Skills 的本质是&quot;知识 + 行为指令&quot;，它通过加载到上下文中来改变 Agent 的思考和行为方式，同时可以驱动 Agent 去调用 Function Call、MCP 工具、执行代码等外部操作&lt;/strong&gt;。这就像给 Agent 临时注入了一段专业经验——不仅告诉它&quot;该怎么想&quot;，还告诉它&quot;该怎么做、用什么工具做&quot;。&lt;/p&gt;
&lt;h3&gt;Skills 有什么价值？&lt;/h3&gt;
&lt;p&gt;Skills 的核心价值在于&lt;strong&gt;将专业知识和最佳实践编码成可复用的模块&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774742429864-39c9a94f-09d8-4242-918f-d654b6ce5cfb.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;举几个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个&quot;代码审查&quot;Skill 可以定义审查的标准流程、关注点（安全性、性能、可读性）、输出格式；&lt;/li&gt;
&lt;li&gt;一个&quot;SQL 优化&quot;Skill 可以编码 DBA 的优化经验，先看执行计划、关注全表扫描、检查索引使用等；&lt;/li&gt;
&lt;li&gt;一个&quot;客服回复&quot;Skill 可以定义品牌话术风格、常见问题处理流程、升级规则等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些经验以前都在人的脑子里，现在可以写成 Skill 文件让 Agent 使用。而且 Skills 可以共享和复用，你写了一个好的 Skill，团队里所有人的 Agent 都能用上。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Function Call、MCP、Skills 有什么区别？&lt;/h2&gt;
&lt;p&gt;好了，前面分别讲了 Function Call、MCP 和 Skills，你可能已经有点绕了，它们不都是&quot;让 Agent 更强&quot;的手段吗？到底有什么区别？&lt;/p&gt;
&lt;p&gt;咱们用一个统一的比喻来把它们串起来，你就彻底明白了。&lt;/p&gt;
&lt;h3&gt;一个统一的比喻&lt;/h3&gt;
&lt;p&gt;想象 Agent 是一个新入职的员工。&lt;strong&gt;Function Call 就是&quot;打电话的能力&quot;&lt;/strong&gt;，这个员工学会了怎么拿起电话、拨号、跟对方沟通。这是最基础的能力，没有这个能力他就没法跟外部世界互动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MCP 就是&quot;公司的通讯录和电话系统&quot;&lt;/strong&gt;，它统一管理所有外部联系方式（供应商、合作伙伴、服务商），员工不需要自己记住每个人的电话号码和通话方式，直接查通讯录就行。新增一个联系人只要加到通讯录里，所有员工都能用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skills 就是&quot;岗位培训手册&quot;&lt;/strong&gt;，它告诉员工&quot;遇到客户投诉应该按什么流程处理&quot;&quot;做报表应该用什么模板和方法&quot;&quot;跟供应商谈判要注意哪些要点&quot;。它教的是做事的方法和规范，而不是打电话的技术。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774741378251-84caba13-b037-4919-9f14-be1563212055.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;三者的本质区别&lt;/h3&gt;
&lt;p&gt;如果用更技术的语言来说，三者的区别体现在几个维度上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774741555822-4d583762-d5bb-4a7b-8a80-bba96f693fb7.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从&lt;strong&gt;解决的问题&lt;/strong&gt;来看，Function Call 解决的是&quot;LLM 怎么跟外部函数交互&quot;这个最基础的问题。MCP 解决的是&quot;怎么用统一标准管理大量工具&quot;的集成问题。Skills 解决的是&quot;Agent 怎么获得领域专业知识&quot;的知识问题。&lt;/p&gt;
&lt;p&gt;从&lt;strong&gt;运行位置&lt;/strong&gt;来看，Function Call 的函数在你的应用程序中执行。MCP 的工具在外部的 MCP Server 中执行。Skills 完全在 Agent 的上下文窗口内生效，不涉及任何外部调用。&lt;/p&gt;
&lt;p&gt;从&lt;strong&gt;技术本质&lt;/strong&gt;来看，Function Call 是一种 API 协议，LLM 输出结构化的调用请求，应用程序执行后返回结果。MCP 是一种通信标准，定义了 Client 和 Server 之间如何发现和调用工具。Skills 是一种提示词扩展，用自然语言编写的行为指令，加载到 Agent 的上下文中。&lt;/p&gt;
&lt;p&gt;从&lt;strong&gt;标准化程度&lt;/strong&gt;来看，Function Call 在各 LLM 厂商之间格式不统一（OpenAI 和 Anthropic 的格式就不一样）。MCP 是统一的开放标准，跨厂商通用。Skills 目前还没有统一标准，各个 Agent 平台有自己的 Skill 格式。&lt;/p&gt;
&lt;h3&gt;三者是什么关系？&lt;/h3&gt;
&lt;p&gt;理解了区别之后，更重要的是理解三者的&lt;strong&gt;协作关系&lt;/strong&gt;，它们不是竞争关系，而是分层互补的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774742269876-85b7e76a-6b99-4a53-9a81-b7b270ea82d3.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Function Call 是&lt;strong&gt;底层基础&lt;/strong&gt;。MCP 建立在 Function Call 之上，提供了标准化的包装。当你的 Agent 通过 MCP 调用一个工具时，底层其实还是在做 Function Call，只不过格式和通信方式被 MCP 统一了。&lt;/p&gt;
&lt;p&gt;Skills 则在一个完全不同的维度上工作，它不参与工具调用的过程，而是指导 Agent&quot;什么时候该调用工具&quot;&quot;用什么策略来完成任务&quot;。&lt;/p&gt;
&lt;p&gt;用做饭来总结：Function Call 是&quot;会使用厨具的能力&quot;（会开火、会切菜），MCP 是&quot;一个设备齐全且标准化的厨房&quot;（所有厨具放在该放的地方，用统一的方式使用），Skills 是&quot;菜谱和厨艺经验&quot;（知道做什么菜、怎么做、火候多大）。三者结合，才能做出一桌好菜。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;什么是 A2A 协议？&lt;/h2&gt;
&lt;p&gt;前面我们讲了 MCP 协议，它解决了 Agent 跟工具之间的连接问题。但还有一个问题 MCP 没有解决，&lt;strong&gt;如果有多个 Agent 需要互相协作，它们之间怎么通信？&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;为什么需要 A2A？&lt;/h3&gt;
&lt;p&gt;想象这样一个场景：一个大型企业里有多个 AI Agent，HR 部门有一个招聘 Agent，IT 部门有一个运维 Agent，财务部门有一个报销 Agent。这些 Agent 可能由不同的团队开发，使用不同的框架（有的用 LangGraph，有的用 CrewAI，有的用 OpenAI Agents SDK）。&lt;/p&gt;
&lt;p&gt;当一个新员工入职时，需要 HR Agent 处理入职手续、IT Agent 配置工作电脑和账号、财务 Agent 设置薪资账户。这三个 Agent 需要协作，但它们互相不认识，也不知道对方会什么、怎么沟通。&lt;/p&gt;
&lt;p&gt;MCP 解决不了这个问题，因为 MCP 是给 Agent 连接&quot;工具&quot;用的，它处理的是&quot;Agent 调用数据库&quot;&quot;Agent 发送邮件&quot;这类 Agent 与工具之间的交互。但 Agent 与 Agent 之间的协作，需要一个全新的协议。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774741746069-daabe0a7-e531-49c1-9ebc-02b89315cf5f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这就是 &lt;strong&gt;A2A（Agent-to-Agent）协议&lt;/strong&gt;诞生的背景。&lt;/p&gt;
&lt;h3&gt;A2A 是什么？&lt;/h3&gt;
&lt;p&gt;A2A 是 Google 在 2025 年 4 月的 Google Cloud Next 大会上发布的一个&lt;strong&gt;开放协议&lt;/strong&gt;，全称 Agent-to-Agent Protocol，顾名思义就是&quot;Agent 对 Agent&quot;的通信协议。它联合了超过 50 家合作伙伴共同推出，包括 Atlassian、Salesforce、SAP、MongoDB、LangChain 等业界大玩家。&lt;/p&gt;
&lt;p&gt;A2A 定义了一套标准，让不同的 AI Agent 能够&lt;strong&gt;互相发现、互相通信、互相委派任务&lt;/strong&gt;，不管这些 Agent 是用什么框架开发的、运行在什么平台上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774712593557-057429ce-0d8f-43d4-99ca-6cdf1ea3ae5d.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;A2A 的核心概念&lt;/h3&gt;
&lt;p&gt;A2A 里有几个关键概念需要了解。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774741862834-8661573f-0685-4807-a64f-f0bd57343a84.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一个是 &lt;strong&gt;Agent Card（智能体名片）&lt;/strong&gt;。每个支持 A2A 的 Agent 都会发布一个 JSON 格式的&quot;名片&quot;，描述自己的身份、能力、擅长的领域、支持的交互方式、认证要求等信息。其他 Agent 通过读取这个名片来了解&quot;这个 Agent 会什么，我能请它帮什么忙&quot;。这就像你在 LinkedIn 上看到一个人的简历，知道他的技能和经验后，才决定要不要跟他合作。&lt;/p&gt;
&lt;p&gt;第二个是&lt;strong&gt;任务（Task）&lt;/strong&gt;。A2A 中的所有协作都围绕&quot;任务&quot;展开。一个 Client Agent（发起方）创建一个任务，发送给一个 Remote Agent（执行方）。任务有完整的生命周期，从创建、处理中、到完成或失败，每个状态变化都有明确的定义。这让双方能清楚地跟踪任务进展。&lt;/p&gt;
&lt;p&gt;第三个是&lt;strong&gt;消息和制品（Message &amp;amp; Artifact）&lt;/strong&gt;。Agent 之间通过消息来沟通过程中的信息，通过制品来传递最终结果。比如一个研究 Agent 完成分析后，会把分析报告作为制品返回给请求方。&lt;/p&gt;
&lt;h3&gt;A2A 和 MCP 是什么关系？&lt;/h3&gt;
&lt;p&gt;这是一个非常重要的问题，很多人会搞混。&lt;/p&gt;
&lt;p&gt;简单来说：&lt;strong&gt;MCP 是「竖向」的，处理 Agent 到工具的连接；A2A 是「横向」的，处理 Agent 到 Agent 的协作。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Google 的官方设计中，两者是互补的关系，被明确设计为可以共同使用。在一个典型的多 Agent 系统中，一个编排 Agent 通过 A2A 协议把任务委派给不同的专业 Agent，而每个专业 Agent 内部通过 MCP 来连接它自己需要的工具。协议的边界很清晰：Agent 之间的通信走 A2A，Agent 调用工具走 MCP。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774741973091-bdb6d573-d833-4fda-9bec-60fc0dabc1f6.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;A2A 的技术特点&lt;/h3&gt;
&lt;p&gt;A2A 有几个值得注意的设计特点。&lt;/p&gt;
&lt;p&gt;首先它完全基于现有的 Web 标准，HTTP 用于传输、JSON 用于消息格式、SSE 用于实时流式通信。这意味着不需要引入任何新的基础设施，现有的 Web 技术栈就能直接支持。&lt;/p&gt;
&lt;p&gt;其次它支持&lt;strong&gt;异步和长时间运行的任务&lt;/strong&gt;。Agent 之间的协作往往不是一瞬间能完成的，一个研究 Agent 可能需要几个小时才能完成深度调研。A2A 对此有完整的支持，包括任务状态查询、进度更新、断线重连等。&lt;/p&gt;
&lt;p&gt;另外它是&lt;strong&gt;模态无关的&lt;/strong&gt;，Agent 之间不仅能传递文本，还能交换音频、视频、图像、结构化数据等各种格式的内容。&lt;/p&gt;
&lt;h3&gt;A2A 的生态现状&lt;/h3&gt;
&lt;p&gt;截至 2026 年初，A2A 还处于发展的早期阶段，远不如 MCP 那样已经成为事实标准。&lt;/p&gt;
&lt;p&gt;但它背后有 Google 和 50 多家合作伙伴的推动，而且它填补了一个 MCP 明确不处理的空白领域（Agent 间协作），所以业界普遍看好它的前景。&lt;/p&gt;
&lt;p&gt;目前整个 Agent 协议生态正在形成一个清晰的分层格局：&lt;strong&gt;Function Call 提供底层调用能力，MCP 标准化 Agent 与工具的连接，A2A 标准化 Agent 与 Agent 的协作&lt;/strong&gt;。三层协议各司其职、互补共存，共同支撑起 Agent 技术的完整基础设施。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;好了，到这里八个核心问题就都讲清楚了。&lt;/p&gt;
&lt;p&gt;我们快速回顾一下整篇文章的脉络：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从最基础的 &lt;strong&gt;LLM&lt;/strong&gt; 说起，它是一个超级强大的文本生成引擎，但有「不能做事、没有记忆、不会用工具、不会规划」四大弊端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt; 在 LLM 基础上加入了工具、记忆、规划能力，从「回答问题」进化到「完成任务」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow&lt;/strong&gt; 和 Agent 的区别在于「谁在控制流程」，Workflow 由代码控制，Agent 由 LLM 自主决策。&lt;/li&gt;
&lt;li&gt;Agent 有多种&lt;strong&gt;工作模式&lt;/strong&gt;，ReAct 边想边做、Plan-and-Execute 先想后做、Reflection 做完检查、Multi-Agent 团队协作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在底层技术上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Function Call&lt;/strong&gt; 是让 LLM 能调用外部函数的基础能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt; 是统一 Agent 与工具连接的标准协议，被称为「AI 界的 USB-C」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; 是教 Agent 做事方法的知识层，解决「怎么做」的问题。三者分别对应能力层、连接层和知识层，分层互补。&lt;/li&gt;
&lt;li&gt;最后 &lt;strong&gt;A2A&lt;/strong&gt; 协议解决了 Agent 之间如何互相发现和协作的问题，与 MCP 形成「横向+竖向」的完整协议生态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好了，今天的分享就到这里。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/concept/agent.html&quot;&gt;agent/concept/agent.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🔄Loop Engineering 是什么？AI 编程从 Prompt 到 Loop 的范式转变</title><link>https://www.corgi37.top/posts/loop-engineering-fanshi-zhuanbian/</link><guid isPermaLink="true">https://www.corgi37.top/posts/loop-engineering-fanshi-zhuanbian/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;不知道你有没有跟我一样的感觉：AI 圈造新词的速度，已经超过我学习的速度了。&lt;/p&gt;
&lt;p&gt;前年说会用 AI 的关键是 prompt engineering，赶紧学；去年又说 prompt 过时了，现在流行 context engineering，行，接着学；今年 3 月冒出来一个 harness engineering，估计还有很多同学没整明白 harness 到底是什么。&lt;/p&gt;
&lt;p&gt;现在这才 6 月，又来了，这次的新词叫 &lt;strong&gt;loop engineering&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;讲真，我第一反应是翻白眼：又来？是不是把 engineering 前面换个单词就能造一个赛道？&lt;/p&gt;
&lt;p&gt;但我把来龙去脉扒了一圈之后，我收回了白眼。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/01-ai-term-timeline-a8b9b29f.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个词不是哪个营销号造的。&lt;/p&gt;
&lt;p&gt;点火的是 Peter Steinberger，开源 agent 项目 OpenClaw 的作者，他 6 月 7 日发了条推文，说：「每月例行提醒：你不该再给 coding agent 打 prompt 了。你该去设计那个给 agent 打 prompt 的 loop。」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260611221837682.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;给概念定名写长文的是 Addy Osmani，Google Cloud 的 AI 总监：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260611222149377.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而几天前，Claude Code 创始人 Boris Cherny 在访谈里说了一句更猛的话，等于提前给这个概念背了书：&lt;/p&gt;
&lt;p&gt;「我已经不 prompt Claude 了。是 loop 在运行着 prompt Claude、决定做什么。我的工作是写 loop。」&lt;/p&gt;
&lt;p&gt;一个造工具的、一个定方法论的、一个每天泡在一线的产品创始人，三个人在同一周说了同一件事。&lt;/p&gt;
&lt;p&gt;这种程度的共振，值得认真看一看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/02-three-source-cards-f073ff34.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这篇文章我整理成 6 个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q1：四个 engineering，到底都在 engineering 什么？&lt;/li&gt;
&lt;li&gt;Q2：Loop Engineering 到底是什么？&lt;/li&gt;
&lt;li&gt;Q3：一个 loop 由什么组成？&lt;/li&gt;
&lt;li&gt;Q4：拼起来之后，一个真实的 loop 长什么样？&lt;/li&gt;
&lt;li&gt;Q5：工具已经追上来了，现在就能搭&lt;/li&gt;
&lt;li&gt;Q6：三盆冷水：loop 越好用，这三个问题越尖锐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们一个一个来说。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Q1：四个 engineering，到底都在 engineering 什么？&lt;/h2&gt;
&lt;p&gt;在讲 loop 之前，得先把欠的账还了：prompt、context、harness 这三个词，很多人到现在也只是「听过」，没真正分清。&lt;/p&gt;
&lt;p&gt;先问一个问题：为什么这些词会一个接一个地冒出来？是 AI 圈闲得慌吗？&lt;/p&gt;
&lt;p&gt;还真不是。每个新词的出现，背后都是同一件事：&lt;strong&gt;上一个瓶颈被解决了，新的瓶颈暴露出来了&lt;/strong&gt;。把这条线从头捋一遍，四个词一下就清楚了。&lt;/p&gt;
&lt;p&gt;时间回到 2023 年。那时候模型只会一问一答，你问得好不好，直接决定答得好不好。&lt;/p&gt;
&lt;p&gt;于是大家研究话术：角色扮演、思维链、少样本示例。&lt;/p&gt;
&lt;p&gt;这就是提示词工程（Prompt Engineering），本质是&lt;strong&gt;跟一个聪明但一根筋的实习生说话的技巧&lt;/strong&gt;，同一件事换个问法，效果天差地别。这个阶段的瓶颈，卡在「怎么说」。&lt;/p&gt;
&lt;p&gt;但话术的红利吃不了太久。到了 2025 年，模型变强了，也开始当 agent 干活了，光会说话不够用了。&lt;/p&gt;
&lt;p&gt;你让它改一个 bug，它写得再漂亮也没用，因为它没看过你的代码、不知道你的规范、不了解之前的讨论。话术解决不了「巧妇难为无米之炊」。&lt;/p&gt;
&lt;p&gt;于是瓶颈从「怎么问」移到了「喂什么」：把对的代码、文档、工具、历史记忆，在对的时机塞进上下文窗口。这就是上下文工程（Context Engineering），当时 Shopify 的 CEO 和 Karpathy 先后带火了这个说法。&lt;/p&gt;
&lt;p&gt;类比一下：你不再纠结怎么跟实习生说话，而是开始给他准备一桌整理好的资料。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/03-prompt-vs-context-fc4d6ca0.png!v1&quot; alt=&quot;prompt engineering 与 context engineering 对比图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;prompt engineering 与 context engineering 对比图&lt;/p&gt;
&lt;p&gt;材料的问题刚解决，新的短板马上接棒。2026 年初，模型已经能连续干几个小时的活了，这时候卡脖子的不再是材料，而是它干活的「环境」跟不上。&lt;/p&gt;
&lt;p&gt;它需要工具去执行命令、需要权限边界防止误伤、需要沙箱安全地试错、需要派出子 agent 分头探索、需要上下文管理机制防止越干越糊涂。这一整套围绕模型搭建的运行装备，业内叫 &lt;strong&gt;harness&lt;/strong&gt;（直译是马具，可以理解成 agent 的「驾驶舱」）。&lt;/p&gt;
&lt;p&gt;今年 3 月前后，Anthropic、OpenAI、LangChain 几家几乎同时发文章讨论这件事，还有人给出了一个很好记的公式：&lt;strong&gt;Agent = 模型 + harness&lt;/strong&gt;。同一个模型，驾驶舱不一样，能力可以差出几倍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/04-agent-model-harness-7e122580.png!v1&quot; alt=&quot;Agent 等于模型加 harness 公式拆解图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Agent 等于模型加 harness 公式拆解图&lt;/p&gt;
&lt;p&gt;话术、材料、驾驶舱，三道坎都迈过去了。那最后剩下的瓶颈是谁？&lt;/p&gt;
&lt;p&gt;你自己。&lt;/p&gt;
&lt;p&gt;模型在等你布置任务，harness 在等你启动，材料在等你投喂。整条流水线上，&lt;strong&gt;唯一还需要人肉驱动的环节，就是你坐在屏幕前敲下一条 prompt&lt;/strong&gt;。你睡觉，它就停工。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/05-human-enter-bottleneck-7e7a1ab8.png!v1&quot; alt=&quot;最后瓶颈示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后瓶颈示意图&lt;/p&gt;
&lt;p&gt;loop engineering 瞄准的就是这最后一环：设计一个系统，让「下一次回车」不再由你来按。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/06-four-engineering-layers-acc3986a.png!v1&quot; alt=&quot;四代概念演进总览图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;四代概念演进总览图&lt;/p&gt;
&lt;p&gt;看出规律了吗？&lt;strong&gt;模型每变强一截，瓶颈就往外移一层&lt;/strong&gt;：从你说的那句话，到你给的那堆材料，到它干活的环境，最后落到你本人身上。&lt;/p&gt;
&lt;p&gt;所以这些词不是营销轮换，是瓶颈迁移的路标。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;四个 engineering，本质是同一场瓶颈迁移：最后一个瓶颈，是坐在键盘前的你。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Q2：Loop Engineering 到底是什么？&lt;/h2&gt;
&lt;p&gt;铺垫完了，现在正面回答：loop engineering 是什么？&lt;/p&gt;
&lt;p&gt;开头 Steinberger（OpenClaw 的作者） 那条 800 多万浏览的推文，只负责把口号喊响：别再 prompt 了，去设计 loop。但口号当不了定义。&lt;/p&gt;
&lt;p&gt;紧接着，Osmani（Google Cloud 的 AI 总监） 的长文给出了正式定义：&lt;/p&gt;
&lt;p&gt;「Loop engineering 就是把『亲自给 agent 写 prompt 的那个你』替换掉。你转而去设计那个代替你做这件事的系统。」&lt;/p&gt;
&lt;p&gt;他还补了一句对 loop 本身的解释：loop 可以理解为一个&lt;strong&gt;递归式的目标&lt;/strong&gt;，你定义一个目的，AI 持续迭代，直到完成。&lt;/p&gt;
&lt;p&gt;说人话就是：过去两年，你跟 coding agent 的协作方式是回合制的，你写一条 prompt，读它的输出，再写下一条。agent 是工具，&lt;strong&gt;你全程握着它&lt;/strong&gt;，一回合都不能松手。&lt;/p&gt;
&lt;p&gt;loop engineering 说的是，松手吧。你把「发现任务、布置任务、检查结果、决定下一步」这套流程设计成一个能自己运转的循环，然后让循环去握着 agent。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/07-turn-based-vs-loop-98da3416.png!v1&quot; alt=&quot;回合制 vs 循环制对比图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回合制 vs 循环制对比图&lt;/p&gt;
&lt;p&gt;打个比方。以前你是客服热线的接线员，每个电话都要你亲自接、亲自答；现在你升级成了设计工单系统的人：电话怎么分流、哪类问题转给谁、办结标准是什么、办不了的怎么升级到你，规则定好，系统自己转。&lt;/p&gt;
&lt;p&gt;你没有离开这家公司，但你的岗位变了。&lt;/p&gt;
&lt;p&gt;这也是 Claude Code 创始人说的那句「我的工作是写 loop」的真实含义。注意，他没说工作变轻松了。这句话真正的重点是：&lt;strong&gt;工作没有变容易，是杠杆的支点移动了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;什么叫支点移动？以前你写一条好 prompt，收益是「这一次回答变好」；现在你设计一个好 loop，收益是「之后每一次循环都变好」。投入从消耗品变成了资产。&lt;/p&gt;
&lt;p&gt;但反过来，设计 loop 也比写 prompt 难得多：你要考虑触发、并行、验证、状态、止损，相当于从「说一句话」升级到「设计一套制度」。杠杆变长了，对握杠杆的人要求也变高了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/08-lever-pivot-shift-f9a5b374.png!v1&quot; alt=&quot;杠杆支点移动示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;杠杆支点移动示意图&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;loop engineering 一句话：你不再是 prompt 的作者，你是 prompt 生产系统的设计师。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Q3：一个 loop 由什么组成？&lt;/h2&gt;
&lt;p&gt;概念清楚了，落地的问题马上来了：一个能自己运转的 loop，到底需要哪几样东西？&lt;/p&gt;
&lt;p&gt;把那些真正跑起来的 loop 拆开看，你会发现零件出奇地一致：&lt;strong&gt;五大件，外加一个记东西的地方&lt;/strong&gt;。我们一件一件过，每一件都对应一个「不装它就会翻车」的具体场景。&lt;/p&gt;
&lt;h3&gt;第一件：自动化，loop 的心跳&lt;/h3&gt;
&lt;p&gt;先想一个问题：你写了一个很完美的工作流脚本，但每次都要你手动启动，它算 loop 吗？&lt;/p&gt;
&lt;p&gt;不算。&lt;strong&gt;自动化才让 loop 成为真正的 loop，否则它只是一个你跑过一次的任务&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以第一件就是定时或事件触发：每天早上自动扫一遍 CI 失败、每次 PR 合并自动跑一轮检查。心跳有了，循环才算活着。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/09-automation-heartbeat-383711c7.png!v1&quot; alt=&quot;自动化心跳示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;自动化心跳示意图&lt;/p&gt;
&lt;h3&gt;第二件：worktree，让并行不变成打架&lt;/h3&gt;
&lt;p&gt;loop 一旦跑起来，经常是几个 agent 同时干活。这时候你会撞上一个特别具体的麻烦：两个 agent 同时改同一个文件。&lt;/p&gt;
&lt;p&gt;就像两个工程师挤在同一台电脑上改同一行代码，还互相不打招呼。&lt;/p&gt;
&lt;p&gt;解法是 git 的 worktree 机制：给每个 agent 一个独立的工作目录和独立分支，共享同一份仓库历史，但物理上互不干扰。各干各的，最后各开各的 PR。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/10-worktree-parallel-isolation-9ce3cc03.png!v1&quot; alt=&quot;worktree 并行隔离示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;worktree 并行隔离示意图&lt;/p&gt;
&lt;h3&gt;第三件：skill，治好 agent 的「金鱼记忆」&lt;/h3&gt;
&lt;p&gt;agent 有个天生缺陷：每个会话都是冷启动，你项目里的规范、约定、坑，它一概不知。&lt;/p&gt;
&lt;p&gt;于是你不得不&lt;strong&gt;像对金鱼一样，每个会话把项目重新解释一遍&lt;/strong&gt;。更要命的是，你没解释到的地方，它不会空着，它会用一个自信的猜测填上。&lt;/p&gt;
&lt;p&gt;skill 就是把这些项目知识写成文件放在仓库里，让 agent 该用的时候自己读。这件事对 loop 的意义比对单次会话大得多：没有 skill，loop 每个周期都从零重新推导你的项目；有了 skill，知识是&lt;strong&gt;复利&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/11-skill-compound-knowledge-afa405bd.png!v1&quot; alt=&quot;从零重推 vs 知识复利对比曲线图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从零重推 vs 知识复利对比曲线图&lt;/p&gt;
&lt;h3&gt;第四件：connector，让 loop 摸到真实世界&lt;/h3&gt;
&lt;p&gt;一个只能看见文件系统的 loop，撑死了算半个 loop。&lt;/p&gt;
&lt;p&gt;真实的工作流不止于代码：要读 issue 工单、查监控、发消息、开 PR。connector（基于 MCP 协议的连接器）就是把这些外部系统接进来的桥。&lt;/p&gt;
&lt;p&gt;接上之后的差别有多大？一个 agent 只会告诉你「修复方案在这里」，而一个完整的 loop 会&lt;strong&gt;自己开好 PR、关联好工单，等 CI 变绿之后自己去频道里通知人&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/12-connector-boundary-9ebae42f.png!v1&quot; alt=&quot;小 loop vs 大 loop 视野对比图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;小 loop vs 大 loop 视野对比图&lt;/p&gt;
&lt;h3&gt;第五件：sub-agent，写的人和查的人必须分开&lt;/h3&gt;
&lt;p&gt;五大件里最有用的结构性设计，我认为遥遥领先的一条，是这个：&lt;strong&gt;把写代码的 agent 和检查代码的 agent 分开&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为什么？理由只有一句话，但谁听谁服：&lt;strong&gt;写代码的那个模型，给自己的作业打分时，实在太手下留情了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;让 A 出方案，让一个干净上下文的 B 来挑刺，B 没有「希望自己是对的」的包袱，挑出来的问题才是真问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/13-maker-checker-split-d4173c25.png!v1&quot; alt=&quot;maker 和 checker 分离示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;maker 和 checker 分离示意图&lt;/p&gt;
&lt;h3&gt;第六件：记忆，loop 的命根子&lt;/h3&gt;
&lt;p&gt;最后这件听起来最不起眼，但它是整个 loop 的命根子。&lt;/p&gt;
&lt;p&gt;问题是这样的：模型在两次运行之间会忘掉一切。今天的循环干了什么、哪些做完了、哪些卡住了，明天的循环一概不知道。&lt;/p&gt;
&lt;p&gt;解法朴素到让人意外：&lt;strong&gt;把记忆放在磁盘上，而不是上下文里&lt;/strong&gt;。一个 markdown 文件、一个任务看板，什么都行，只要它活在单次对话之外，记录着「做完了什么、下一步是什么」。&lt;/p&gt;
&lt;p&gt;这件事，Osmani 的博客里留了一句很妙的总结：&lt;/p&gt;
&lt;p&gt;「agent 会忘，但 repo 不会。」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260612132111506.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/14-loop-six-parts-memory-3d8a8964.png!v1&quot; alt=&quot;五大件加记忆的全家福图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;五大件加记忆的全家福图&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;五大件让 loop 转得起来，磁盘上的记忆让它第二天还接得上。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Q4：拼起来之后，一个真实的 loop 长什么样？&lt;/h2&gt;
&lt;p&gt;零件都认识了，该看整车了。&lt;/p&gt;
&lt;p&gt;Osmani 在文章里给了一个他自己在用的 loop，我把流程完整搬过来。这个 loop 的任务是：&lt;strong&gt;每天早上自动把项目里值得修的问题找出来、修好、提交审核&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第一步，每天早晨，自动化准时触发，调用一个负责分诊的 skill。&lt;/p&gt;
&lt;p&gt;第二步，这个 skill 去读昨天的 CI 失败记录、还没关闭的 issue、最近的提交，把「哪些问题值得处理」写进一个状态文件。&lt;/p&gt;
&lt;p&gt;第三步，对每一个值得做的问题，开一个隔离的 worktree，派一个 sub-agent 进去起草修复。&lt;/p&gt;
&lt;p&gt;第四步，第二个 sub-agent 登场，对照项目的 skill 规范和现有测试，把那份草稿审一遍。&lt;/p&gt;
&lt;p&gt;第五步，审过了，connector 自动开 PR、更新对应的工单。&lt;/p&gt;
&lt;p&gt;第六步，loop 搞不定的问题，不硬来，丢进一个待办收件箱，等真人来看。&lt;/p&gt;
&lt;p&gt;第七步，所有经过都写回状态文件。明天早上的循环从今天停下的地方继续。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260612132145898.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/15-morning-triage-loop-1eb2f51b.png!v1&quot; alt=&quot;晨间 triage loop 全流程图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;晨间 triage loop 全流程图&lt;/p&gt;
&lt;p&gt;流程走完，你品一品这里面最关键的一件事：&lt;strong&gt;整个过程里，你只设计了一次，中间的任何一步，你都没有写过 prompt&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这就是 Steinberger 那句口号的落地版本。你的活从「每一步都出现」变成了「只在两个地方出现」：设计循环的时候，和收件箱里有东西的时候。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/16-human-position-change-4a2f741d.png!v1&quot; alt=&quot;人的位置变化示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;人的位置变化示意图&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个好 loop 的标志：你只在设计时出现一次，之后只在收件箱前出现。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Q5：工具已经追上来了，现在就能搭&lt;/h2&gt;
&lt;p&gt;听到这里你可能会想：道理是好道理，但搭这么一套系统，工程量不小吧？&lt;/p&gt;
&lt;p&gt;这正是这次概念能火起来的底气所在。这里有一个很关键的时间差：&lt;strong&gt;一年前，搭一个 loop 意味着写一堆只有你自己看得懂、还得永远自己维护的 bash 脚本；而现在，五大件全部内置在主流产品里&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我把两家头部产品的部件整理成一张对照表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;部件&lt;/th&gt;
&lt;th&gt;在 loop 里的职责&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;自动化&lt;/td&gt;
&lt;td&gt;定时发现和分诊&lt;/td&gt;
&lt;td&gt;Automations 面板 + 分诊收件箱&lt;/td&gt;
&lt;td&gt;计划任务、&lt;code&gt;/loop&lt;/code&gt;、hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worktree&lt;/td&gt;
&lt;td&gt;隔离并行任务&lt;/td&gt;
&lt;td&gt;每个线程内置 worktree&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git worktree&lt;/code&gt;、隔离配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;skill&lt;/td&gt;
&lt;td&gt;固化项目知识&lt;/td&gt;
&lt;td&gt;Agent Skills&lt;/td&gt;
&lt;td&gt;Agent Skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;connector&lt;/td&gt;
&lt;td&gt;连接外部工具&lt;/td&gt;
&lt;td&gt;基于 MCP 的 Connectors&lt;/td&gt;
&lt;td&gt;MCP servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sub-agent&lt;/td&gt;
&lt;td&gt;分头干活、写查分离&lt;/td&gt;
&lt;td&gt;配置文件定义子 agent&lt;/td&gt;
&lt;td&gt;子 agent、agent teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;记忆&lt;/td&gt;
&lt;td&gt;追踪进度&lt;/td&gt;
&lt;td&gt;markdown 或接工单系统&lt;/td&gt;
&lt;td&gt;markdown 或接工单系统&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/17-codex-claude-parts-shelf-e0cb24c2.png!v1&quot; alt=&quot;双产品部件对照货架图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;双产品部件对照货架图&lt;/p&gt;
&lt;p&gt;这张表里还藏着一个值得单独拎出来的细节：两家都有一个 &lt;code&gt;/goal&lt;/code&gt; 类的能力，它和普通定时循环的区别很微妙，但很重要。&lt;/p&gt;
&lt;p&gt;普通的循环是&lt;strong&gt;按节奏重复跑&lt;/strong&gt;：每小时跑一次，跑完就完了，对不对另说。而 &lt;code&gt;/goal&lt;/code&gt; 是&lt;strong&gt;跑到你写的条件为真才停&lt;/strong&gt;，比如「目录下所有测试通过且 lint 干净」。更妙的是，每一轮结束后，由一个&lt;strong&gt;独立的模型&lt;/strong&gt;来判断条件是否达成。&lt;/p&gt;
&lt;p&gt;发现没有？这就是上一节说的「写的人和查的人分开」，只不过这次用在了「什么时候算干完了」这个停止条件上。连「我做完了」这句话，都不让干活的那个 agent 自己说。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/18-timer-vs-condition-loop-098b2dff.png!v1&quot; alt=&quot;定时循环 vs 条件循环对比图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;定时循环 vs 条件循环对比图&lt;/p&gt;
&lt;h3&gt;实操：30 秒搭出你的第一个 loop&lt;/h3&gt;
&lt;p&gt;光说不练假把式，我们拿一个所有人都烦过的场景，真刀真枪走一遍。&lt;/p&gt;
&lt;p&gt;这个场景是：你提了个 PR，然后开始等 CI。挂了，切回去看日志、改、推送，再等。一下午切了八次窗口，正经活没干多少。&lt;/p&gt;
&lt;p&gt;在 Claude Code 里，这件事用一条 &lt;code&gt;/loop&lt;/code&gt; 命令就能交出去：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/loop 10m 检查当前分支 PR 的 CI 状态：有失败的检查就读日志、修复、推送；
全部变绿后停下来，给我一句话总结改了什么
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把这条命令拆开看，麻雀虽小，loop 的骨架是全的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;10m&lt;/code&gt; 是心跳：每 10 分钟自动醒来跑一轮，不用你按回车。中间那段是任务：每一轮干什么。最后一句是停止条件加汇报：什么时候算完、完了怎么交差。&lt;/p&gt;
&lt;p&gt;敲下去之后你就可以去干别的了。CI 挂了它自己修，修完自己推，全绿了它叫你。刚才那个来回切窗口的下午，被压缩成了「最后看一眼总结」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/19-loop-command-anatomy-3ace5c7a.png!v1&quot; alt=&quot;loop 命令解剖图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;loop 命令解剖图&lt;/p&gt;
&lt;p&gt;还有个更省心的玩法：把间隔省掉，直接 &lt;code&gt;/loop&lt;/code&gt; 加任务。这时候节奏由模型自己定，它会根据「CI 一般要跑多久」来决定多久看一次，不会傻乎乎地一分钟刷一次。&lt;/p&gt;
&lt;p&gt;两个使用边界也交代清楚，免得你回头骂我。&lt;/p&gt;
&lt;p&gt;第一，&lt;code&gt;/loop&lt;/code&gt; 活在当前会话里，适合「今天盯着这件事」的轮询；你关掉电脑它就停了。想要那种睡觉时也在跑的 loop，要用计划任务或者云端的 routines，让它脱离你的机器运行。&lt;/p&gt;
&lt;p&gt;第二，回头对照 Q3 的五大件你会发现，这个最小 loop 只有心跳、任务和停止条件，没有 worktree、没有写查分离的 sub-agent。这不是缺陷，是起点：先让最小的循环转起来，哪天你觉得「它自己改的代码我不放心」，再把检查的 sub-agent 加上；觉得「想同时盯三个 PR」，再上 worktree。&lt;strong&gt;部件是一件一件长出来的，不是一天配齐的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/20-loop-evolution-roadmap-b1d50ff5.png!v1&quot; alt=&quot;loop 进化路线图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;loop 进化路线图&lt;/p&gt;
&lt;p&gt;而对照表还说明了一件更大的事：两家产品的部件几乎一一对应，&lt;strong&gt;loop 的设计正在变得工具无关&lt;/strong&gt;。部件是同样的部件，差的只是商标。&lt;/p&gt;
&lt;p&gt;这意味着什么？意味着「选 Codex 还是选 Claude Code」这种争论的重要性在下降。loop 的设计图纸是你的资产，画好了，放在哪家的产品上都能转。值得积累的是图纸，不是对某家工具的肌肉记忆。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/21-loop-blueprint-tool-agnostic-e3c5e6ec.png!v1&quot; alt=&quot;loop 图纸工具无关示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;loop 图纸工具无关示意图&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;门槛已经从「自己造零件」降到了「学会拼装」，剩下的问题只是你想让 loop 替你做什么。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Q6：三盆冷水：loop 越好用，这三个问题越尖锐&lt;/h2&gt;
&lt;p&gt;文章到这里都挺振奋的，该泼冷水了。&lt;/p&gt;
&lt;p&gt;有意思的是，泼得最狠的不是哪个反对派，恰恰是给概念定名的 Osmani 本人，他在博客里直说：「现在还早，我是持怀疑态度的。」&lt;/p&gt;
&lt;p&gt;冷水的核心是一句话：&lt;strong&gt;loop 改变了工作，但没有把你从工作中删除&lt;/strong&gt;。而且有三个问题，会随着 loop 越来越好用，变得越来越尖锐，而不是越来越轻松。&lt;/p&gt;
&lt;h3&gt;第一盆：验证仍然归你&lt;/h3&gt;
&lt;p&gt;loop 无人值守地运行，听起来很美。但换个角度念这句话：&lt;strong&gt;一个无人值守运行的 loop，也是一个无人值守犯错的 loop&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你睡觉时它在干活，也意味着你睡觉时它在犯错。&lt;/p&gt;
&lt;p&gt;就算你按规矩配了负责检查的 sub-agent，也别高兴太早：&lt;strong&gt;检查 agent 嘴里的「done」，只是一个声明，不是一个证明&lt;/strong&gt;。它说没问题，和真的没问题，中间还隔着你的眼睛。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/22-claim-vs-proof-8af1784b.png!v1&quot; alt=&quot;声明 vs 证明对比图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;声明 vs 证明对比图&lt;/p&gt;
&lt;h3&gt;第二盆：理解债，越顺滑涨得越快&lt;/h3&gt;
&lt;p&gt;第二个问题更隐蔽。loop 交付你没写过的代码越快，「仓库里实际存在的东西」和「你脑子里真正理解的东西」之间的鸿沟就越大。&lt;/p&gt;
&lt;p&gt;这有个专门的名字，叫&lt;strong&gt;理解债（Comprehension Debt）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它和技术债不一样：技术债是代码烂，理解债是代码可能不烂，&lt;strong&gt;但你不知道它为什么是对的&lt;/strong&gt;。出问题的那天，你面对的是一片自己「拥有」但不「理解」的代码。一个顺滑的 loop 不会帮你还这笔债，只会让它涨得更快，除非你坚持去读 loop 产出的东西。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/23-comprehension-debt-gap-dc781e96.png!v1&quot; alt=&quot;理解债鸿沟示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;理解债鸿沟示意图&lt;/p&gt;
&lt;h3&gt;第三盆：认知投降，最舒服的姿势最危险&lt;/h3&gt;
&lt;p&gt;第三个问题最扎心。loop 自己转起来之后，你会发现一个特别舒服的姿势：不再对产出有自己的观点，它给什么就收什么。&lt;/p&gt;
&lt;p&gt;这个状态有个专门的名字，叫&lt;strong&gt;认知投降（Cognitive Surrender）&lt;/strong&gt;。Osmani 的博客里有一段关于它的原话，非常锋利：&lt;/p&gt;
&lt;p&gt;「带着判断力去设计 loop，它是解药；为了逃避思考去设计 loop，它是助燃剂。同一个动作，相反的结果。」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/24-cure-vs-accelerant-cbff7104.png!v1&quot; alt=&quot;解药与助燃剂双面图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解药与助燃剂双面图&lt;/p&gt;
&lt;p&gt;最后还有一笔很现实的账：token 成本。loop 是按循环烧 token 的，sub-agent 每多一个，就多一份模型和工具的开销。比较务实的花法，是把 sub-agent 用在「值得买第二意见」的地方，而不是处处双保险。token 富裕和精打细算这两种人，会设计出两种完全不同的 loop。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这三盆冷水有个共同点：它们都不是 loop 的 bug，而是 loop 的代价，并且由你来付。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;把整篇浓缩成 3 句话送你：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一，从 prompt 到 context 到 harness 再到 loop，四个词是一场瓶颈迁移：模型越强，瓶颈越往外移，最后移到了「亲自按回车的你」身上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二，一个 loop 等于五大件加一份磁盘记忆：自动化是心跳，worktree 防打架，skill 治金鱼记忆，connector 摸到真实世界，sub-agent 写查分离，状态文件让明天接得上今天。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三，loop 把你的杠杆变长了，但验证、理解债、认知投降这三笔账也同时变大了，工具分不出你是在加速还是在逃避，你自己分得出。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Osmani 文章的结尾有一段话，我觉得是整场讨论里最值得带走的：&lt;/p&gt;
&lt;p&gt;「两个人可以搭一模一样的 loop，得到完全相反的结果。一个用它在自己深刻理解的工作上加速，另一个用它彻底逃避理解工作。loop 分不出区别，你分得出。」&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Peter Steinberger 的原帖：&lt;a href=&quot;https://x.com/steipete/status/2063697162748260627&quot;&gt;https://x.com/steipete/status/2063697162748260627&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Addy Osmani《Loop Engineering》：&lt;a href=&quot;https://addyosmani.com/blog/loop-engineering&quot;&gt;https://addyosmani.com/blog/loop-engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Boris Cherny 访谈《Claude Code &amp;amp; the Future of Engineering》&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/engineering/loop-engineering.html&quot;&gt;agent/engineering/loop-engineering.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🔁Loop Engineering 怎么落地？一条从 0 到 1 的上手路径</title><link>https://www.corgi37.top/posts/loop-engineering-shangshou-zhinan/</link><guid isPermaLink="true">https://www.corgi37.top/posts/loop-engineering-shangshou-zhinan/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;X 上有个叫 Codez 的博主，前阵子发了份《Loop Engineering 实操手册》，近四百万人看过。他开头那句话挺扎心：十个开发者，九个到现在都没写过一个能替自己 prompt 的 loop。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/image-20260624101018608-20260624180608745.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我把这份手册从头到尾啃了一遍，挑出真正能落地的，掰开揉碎讲给你听。&lt;/p&gt;
&lt;p&gt;废话不多说，发车。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;先别急，你真需要 loop 吗？&lt;/h2&gt;
&lt;p&gt;看完概念，你手可能已经痒了，想立马开一个 loop 让它替你打工。&lt;/p&gt;
&lt;p&gt;别急着上手。&lt;/p&gt;
&lt;p&gt;loop 这东西不是白来的。它烧 token、要花时间搭，真出了岔子，你还得去 debug 一个自己压根没盯着它跑过的系统。说白了，你这是拿真金白银加时间，赌它将来能帮你赚回来。这笔赌划不划算，动手前我建议你拿几个问题，先把自己问一遍。&lt;/p&gt;
&lt;p&gt;第一个，也最实在：这活你是不是每周都要干好几遍？要是一锤子买卖，老老实实写个好 prompt 更快更省，搭 loop 纯属杀鸡用牛刀。&lt;/p&gt;
&lt;p&gt;第二个，我觉得是条生死线：有没有一个东西能自动告诉你「这活砸了」？测试也行，类型检查、linter、跑个构建也行，随便哪个能当裁判都成。没有这个裁判，loop 吐出来的东西你还得一行行读 diff 去验。那它到底替你省了啥？&lt;/p&gt;
&lt;p&gt;第三个是钱：你的 token 预算扛得住浪费吗？loop 会反复读上下文、重试、试探，不出活也照样烧。预算紧的人，这笔空转烧得肉疼。&lt;/p&gt;
&lt;p&gt;第四个：agent 能不能跑它自己写的代码？得有日志、能复现、崩在哪看得见。要不然它两眼一抹黑，自己都不知道刚闯了啥祸，根本没法自我修正。&lt;/p&gt;
&lt;p&gt;这几个之外，还有个附加题，我觉得比上面都狠：你到底打算 review 它产出的东西吗？不打算？那别建，真的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/99d55526_loop-decision-flow.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还得算笔不太中听的账。loop 这东西，说到底是偏向「花得起钱的人」的。&lt;/p&gt;
&lt;p&gt;token 管够的人玩它觉得真香，背着消费套餐的人呢，跑个两轮可能就触顶了，要么月底收到一张吓人的账单。&lt;/p&gt;
&lt;p&gt;所以一句可能不讨喜的大实话：&lt;strong&gt;loop 是真东西，但大部分人现在还用不上&lt;/strong&gt;。这不是泼冷水，是帮你省下一次「搭了个寂寞」。&lt;/p&gt;
&lt;p&gt;还有几类活，再馋也别碰：架构、鉴权、支付、生产部署，一出事就是大事，必须有人盯着。&lt;/p&gt;
&lt;p&gt;至于哪些活适合拿来开张，下一节讲场景时一起说。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第一个 loop 该怎么搭？&lt;/h2&gt;
&lt;p&gt;上面这些都想清楚了，确实该搭。&lt;/p&gt;
&lt;p&gt;这时候新手最容易犯的错，是一上来就想搭个无所不能的系统：自动化、并行、写查分离、连一堆外部工具，恨不得一步到位。&lt;/p&gt;
&lt;p&gt;结果就是搭到一半被各种联调问题劝退，或者搭出来一个根本没法 debug 的庞然大物。&lt;/p&gt;
&lt;p&gt;正确的姿势是反过来：先搭一个能跑起来的最小版，就四样东西。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/b61a7d79_all-in-one-vs-minimal.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一样，automation，也就是 loop 的心跳。按节奏自动触发，定时也行，某个事件也行。这里有条铁律千万别忘：停止条件一定要写死。不然 token 烧光了，它还在那傻乎乎兜圈子。&lt;/p&gt;
&lt;p&gt;第二样，skill，把项目背景存下来。用什么框架、有什么约定、踩过哪些坑，统统写进一个 skill 文件丢仓库里，agent 每轮自己读，省得你一遍遍从头解释。&lt;/p&gt;
&lt;p&gt;第三样，状态文件，专门记「做完了啥、下一步干啥」，好让明天的循环接得上今天。&lt;/p&gt;
&lt;p&gt;为什么单把状态文件拎出来说？因为它是命根子。&lt;/p&gt;
&lt;p&gt;agent 这东西，是个金鱼记忆。这次会话学到的，明天一重启就忘得干干净净。&lt;/p&gt;
&lt;p&gt;我看到有句话点得特别透：agent 会忘，但 repo 不会。你把进度写进文件，loop 才接得上昨天，不然每天都是从头来过。&lt;/p&gt;
&lt;p&gt;这文件放哪，看你情况。&lt;/p&gt;
&lt;p&gt;最简单的办法，就是在仓库里放一个 &lt;code&gt;STATE.md&lt;/code&gt;，跟代码一起管，改了还能 diff，谁打开都看得懂，个人或者小团队用这个就够了。&lt;/p&gt;
&lt;p&gt;要是想更正式一点，就接到外部系统里，Linear（研发项目管理工具）、GitHub Issues、甚至一张数据库表都行，好处是能跨好几个仓库、随时能查，团队里每个人都看得到 loop 在做什么，正经上生产的 loop 更适合这么放。&lt;/p&gt;
&lt;p&gt;它长什么样并不复杂，一个极简的 &lt;code&gt;STATE.md&lt;/code&gt; 大概就这意思：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# STATE.md
## 进行中
- 升级 lodash 到 4.x，本地测试已过，等 CI
## 今天完成
- 修掉登录接口的空指针，已合并
## 卡住了，等人看
- 支付回调偶发超时，复现不稳定，先挂起
## 下一步
- 扫一遍本周新开的 issue，挑能自动改的
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;光有状态还不够。loop 跑久了容易跑偏，所以最好再放一个常驻的目标文件（&lt;code&gt;VISION.md&lt;/code&gt; 或者 &lt;code&gt;AGENTS.md&lt;/code&gt;），每轮开工前先让它读一遍。&lt;/p&gt;
&lt;p&gt;你可以这么理解。状态文件记的是它现在做到哪了。目标文件不一样，管的是它最终要去哪。一个盯当下，一个盯方向。少了哪个，它都会跑着跑着就忘了自己在干嘛。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/8859cc93_state-vs-vision.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第四样，闸门，就是前面反复念叨的那个裁判：测试、类型检查、构建，过不了就直接拦下。这是 loop 唯一能挡住烂活的关卡。这道千万省不得。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/45b2c8fc_minimal-four-piece.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;四样凑齐了，还有件事比凑齐零件更要命：顺序别搞反。&lt;/p&gt;
&lt;p&gt;正确的走法是，先让一次手动运行稳稳跑通，再把背景沉淀成 skill，再包成 loop，最后才接上自动调度。一步稳了，再上下一步。&lt;/p&gt;
&lt;p&gt;非要跳着来，手动都没跑顺就急吼吼上调度，回头出了岔子，你根本分不清是哪一环的锅。&lt;/p&gt;
&lt;p&gt;先把路走稳。再谈自动。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/ee4b07ea_build-order-stairs.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;完整的 loop 还缺哪几块？&lt;/h2&gt;
&lt;p&gt;最小四件套能转起来，但它还很素，只能干最简单的活。想让它干得更多、更稳，就往上加。&lt;/p&gt;
&lt;p&gt;一个完整的 loop，手册拆成五个构件，我先简单过一遍各是干嘛的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动化&lt;/strong&gt;：loop 的心跳。最小版那个 automation 就是它，按时间或事件踢它一脚，它就跑一轮。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;worktree&lt;/strong&gt;：多个 agent 一起干活时，各分一间独立工作区，免得它俩改同一个文件、打起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;skill&lt;/strong&gt;：项目背景、约定、踩过的坑，全塞进文件，agent 每轮自己翻，不用你一遍遍念。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;connector&lt;/strong&gt;：走 MCP 接上 GitHub、Slack、Jira 这些，loop 才能真去开 PR、发消息、查告警，而不是干瞪眼。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sub-agent&lt;/strong&gt;：写代码的和验收的，拆成两个 agent。别让同一个 agent 给自己的作业打分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好在这五样，工具里基本都给你备好了，不用自己造轮子。&lt;/p&gt;
&lt;p&gt;就拿 Claude Code 说：自动化是 &lt;code&gt;/loop&lt;/code&gt; 配上桌面端定时任务和云端 Routines；worktree 直接内置；skill 就是个 &lt;a href=&quot;http://SKILL.md&quot;&gt;SKILL.md&lt;/a&gt; 文件；connector 走 MCP。现成的零件，你把它们拼起来就完事。&lt;/p&gt;
&lt;p&gt;这五个里，要我说，最该刻进脑子的是 sub-agent，也就是「写代码的和验收的分家」。&lt;/p&gt;
&lt;p&gt;说个有点打脸的冷知识：这套「一个 agent 写、另一个 agent 挑刺」的玩法，听着特别 2026，其实 Anthropic 早在 2024 年底就写进工程博客了，叫 evaluator-optimizer。换个新名字，又火一遍。&lt;/p&gt;
&lt;p&gt;道理特别朴素。让写代码的模型给自己的活打分？它准手下留情，跟学生自己批自己的卷子一个样。换一个干净上下文、带着不同指令的第二个 agent 来挑刺，才揪得出第一个它自己骗过自己的毛病。&lt;/p&gt;
&lt;p&gt;loop 是在你不看的时候跑的。一个你真信得过的验证器，是你敢撒手走开的唯一底气。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/3aef445a_five-components-table.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;loop 到底能拿来干嘛？&lt;/h2&gt;
&lt;p&gt;光认识零件还不够，你得知道它们拼起来能替你干什么。&lt;/p&gt;
&lt;p&gt;我把见过的、真能跑起来的 loop 场景，按从轻到重分了三档，你对号入座。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最轻的一档，盯自己手头的活。&lt;/strong&gt; 这档不用搭什么系统，一条命令就行，适合所有人。&lt;/p&gt;
&lt;p&gt;比如你提了个 PR 在等 CI，与其自己每隔几分钟切回去看一眼，不如一条命令丢给它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/loop 10m 检查当前分支 PR 的 CI：有失败就去读日志、修掉、推送；全绿就停下来，给我一句话总结改了什么
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令拆开看就三段：开头的 &lt;code&gt;10m&lt;/code&gt; 是心跳，每 10 分钟自己醒一次；中间那段是每轮要干的活；最后一句是停止条件加交差方式。敲下去你就能去忙别的，CI 挂了它自己修，全绿了再回来叫你。&lt;/p&gt;
&lt;p&gt;把任务换一换，同一个 &lt;code&gt;/loop&lt;/code&gt; 还能盯别的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/loop 监控这次部署，跑完通知我
/loop 每天早上用 Slack 把我昨天被 @ 的消息汇总一下
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意第二条没写时间间隔，这时候节奏就交给模型自己定，它会按任务性质挑个合理的频率，不用你操心。&lt;/p&gt;
&lt;p&gt;这些活的共同点是：本来得你时不时切回去瞄一眼，现在交给 loop，你专心干别的，它有事才喊你。&lt;/p&gt;
&lt;p&gt;这里要分清两个命令，踩过坑的都懂。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;/loop&lt;/code&gt; 是绑在当前会话上的&lt;/strong&gt;，你关掉终端它就停了，适合「今天就盯这一件事」的临时轮询；想要那种你都睡了它还在云端跑、关了机也不耽误的，就得用 &lt;strong&gt;&lt;code&gt;/schedule&lt;/code&gt;&lt;/strong&gt; 这类把任务丢到云端的方式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/loop&lt;/code&gt; 陪你白天忙活。&lt;code&gt;/schedule&lt;/code&gt; 不一样，你下班、关了机，它还在云端接着替你盯。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/d9c6c461_loop-vs-schedule.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对了，&lt;code&gt;/loop&lt;/code&gt; 还有个更聪明的兄弟叫 &lt;code&gt;/goal&lt;/code&gt;，这俩的区别值得单独说一下。&lt;/p&gt;
&lt;p&gt;普通的 &lt;code&gt;/loop&lt;/code&gt; 是按固定节奏重复跑：每隔十分钟跑一轮，至于这轮干得对不对，它不管。而 &lt;code&gt;/goal&lt;/code&gt; 是盯着一个你写好的条件跑，比如「所有测试都变绿」，条件不达成就一直跑，达成了才收手。&lt;/p&gt;
&lt;p&gt;更妙的是，这个条件到底达没达成，不是干活的那个 agent 自己说了算，而是另派一个独立的小模型来判。你看，这又是一次「写的和验的分开」，连「我搞定了」这句话，都不让动手的 agent 自己下结论。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/928e1113_loop-vs-goal.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中间一档，每天定时帮你清理。&lt;/strong&gt; 这档就得用上前面那些构件了，适合有点规模的项目。&lt;/p&gt;
&lt;p&gt;最典型的是「晨间分诊」：每天早上自动跑一轮，读昨天挂掉的 CI、还开着的 issue、最近的提交，判断哪些值得处理，写进状态文件，能自己修的修掉提 PR，搞不定的丢进收件箱等你。&lt;/p&gt;
&lt;p&gt;等于你每天到工位，活已经被分好类、好处理的那批甚至已经处理完了。&lt;/p&gt;
&lt;p&gt;类似的还有定时修 CI（每天凌晨把红掉的流水线捞起来修）、依赖升级（定期查有没有新版本，升完跑测试，绿了才提 PR）。&lt;/p&gt;
&lt;p&gt;我见过玩得猛的，一个人挂着 loop，一晚上给三十来个开源仓库自动开依赖升级 PR，早上起来挨个 review 就完事。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最重的一档，让 loop 自己接活、自己验活。&lt;/strong&gt; 这档是给重度玩家的。&lt;/p&gt;
&lt;p&gt;比如开源项目上，有人提了个 issue，loop 自动去读，对照项目的愿景文档判断这需求合不合方向，合的话直接起草一个 PR，还顺手让另一个 agent 先 review 一遍，再交到你面前。从「有人提需求」到「一个审过的 PR 草稿」，中间没你什么事。&lt;/p&gt;
&lt;p&gt;到了这一档，有个让 loop 越跑越好的技巧值得单独说：&lt;strong&gt;内外两层循环&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;内层那个 loop 负责干活，外层那个 loop 专门吸收你每次 review 时的反馈，拿去改进下一轮怎么干。&lt;/p&gt;
&lt;p&gt;说白了，你每一次「这里改改」，都不只修了这一次，而是喂给了系统，让它下回别再犯。这才是把「设计 loop」的复利真正吃满。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/155dc88e_three-scenario-levels.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;你的 loop 是赚是亏？&lt;/h2&gt;
&lt;p&gt;很多人搭完 loop 就傻乐，觉得进入了自动化时代，却没算过一笔账：这东西到底帮你赚了，还是在偷偷亏钱？&lt;/p&gt;
&lt;p&gt;loop 不像写 prompt 对错当场可知，它长期运行、持续烧钱，赚亏得靠一个指标来盯。这个指标就一个：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每个被接受的改动，花了你多少成本。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把 loop 这阵子烧的 token、占的算力，加上你 review 花掉的时间，全算进去。再除以它真正被你合并了的改动数。得出来的，就是单位成本。这个数你得心里有底。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/b4d2efeb_unit-cost-formula.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;配套还有个特别简单的红线：&lt;strong&gt;如果接受率低于 50%，这个 loop 就在亏本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;意思是，它产出十个改动，你看完只有不到五个能用，剩下一多半要么被你打回要么直接扔。那它产出的「废品」消耗的 token 和你的审查精力，已经超过了它帮你省下的那点活。这种 loop，要么赶紧调，要么干脆关掉。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/da327658_loop-economics-balance.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有笔账顺带说一句：sub-agent 是按个烧钱的，你每多挂一个做检查的，就多花一份模型和工具的钱。&lt;/p&gt;
&lt;p&gt;所以别处处都上双保险，「再请一个 agent 来挑刺」这件事，用在那种真正值得买个第二意见的地方就行。token 宽裕的人和精打细算的人，搭出来的 loop 会很不一样。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;没人盯着，安全怎么办？&lt;/h2&gt;
&lt;p&gt;前面都在讲怎么让 loop 跑得好。最后这节，得讲讲怎么让它别给你惹祸。&lt;/p&gt;
&lt;p&gt;loop 最大的卖点是无人值守，你睡觉它干活。但你把这句话反过来念一遍就该警觉了：它在你不看的时候替你干活，&lt;strong&gt;可同样在你不看的时候，它也敞着一个没人守的口子&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;自动开 PR、自动装 skill、自动连外部工具，每一个口子都可能被人钻。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/9b0a2838_unattended-attack-surface.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我给你拎四条红线，建议直接焊死在闸门里，一条都别松。&lt;/p&gt;
&lt;p&gt;第一条，生成的代码没审就上线。闸门里光有测试和构建还不够，得再塞进 SAST（静态安全扫描）、依赖审计、密钥扫描。让安全检查跟功能检查一个待遇，都是上线前必须过的关。&lt;/p&gt;
&lt;p&gt;第二条，也是我觉得最阴的一条：skill 本身就是个注入入口。你以为装个 skill，不就是多份说明书？能有多大风险？讲真，下面这个数据，我第一次看到的时候，手一抖。&lt;/p&gt;
&lt;p&gt;今年 4 月有一篇 arXiv 上的实证研究（《Credential Leakage in LLM Agent Skills》），专门去扒社区里的 skill 安全问题。研究者从一个 skill 平台的十几万个 skill 里抽样了 17022 个来分析，结果发现其中 &lt;strong&gt;520 个会泄露凭证&lt;/strong&gt;，总共揪出 1708 处问题。&lt;/p&gt;
&lt;p&gt;更扎心的是泄露途径：占比最高的不是什么高深的攻击，而是 &lt;strong&gt;debug 日志&lt;/strong&gt;，光是 print 和 console.log 这类把东西打到标准输出的操作，就造成了 73.5% 的泄露。而且这些泄露里，将近九成不需要任何权限就能被利用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/83b52a94_skill-security-data.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我之前写 &lt;a href=&quot;https://mp.weixin.qq.com/s/CIPa2c-JVB-mSI0YDEB7AA&quot;&gt;claude code skills&lt;/a&gt; 那篇讲过，skill 是个装备齐全的工具箱，agent 能自己翻里面的脚本和资料。&lt;/p&gt;
&lt;p&gt;可这话反过来一样成立。要是这工具箱里被人下了毒呢？agent 照样一声不吭、照单全收。这就跟你捡到一个来路不明的 U 盘，眼都不眨直接插进公司电脑，一个道理。所以&lt;strong&gt;任何 skill，在你的 loop 自动装它之前，先把源码从头到尾读一遍&lt;/strong&gt;，别图省事直接挂上去。&lt;/p&gt;
&lt;p&gt;第三条，凭证泄露进日志。正好接着上面那个数据说，既然 debug 日志是泄露的头号途径，那生产上跑的 loop，就把 verbose 日志关了。开发时打满日志方便排查，没问题。但你别让一个 24 小时挂着跑、还连着一堆外部系统的 loop，把密钥哗哗往日志里灌。&lt;/p&gt;
&lt;p&gt;最后一条，权限蔓延，温水煮青蛙的典型。今天为了让 loop 多干件事，给它加个写权限；明天又加一个；一个月下来，它手里的权限大到你自己都说不清它到底能动哪些东西了。解法很笨，但管用：&lt;strong&gt;每 30 天，把它的权限拉出来复审一遍&lt;/strong&gt;，不再需要的，收回来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/f51114d2_four-security-rails.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后：14 步清单带走&lt;/h2&gt;
&lt;p&gt;讲了这么多，我把 Codez 这份手册的 14 步原样给你列一遍，分三段，你照着走就行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一段 · 先搞清楚要不要做&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;想明白本质：你要做的不是 prompt agent，是设计那个替你 prompt 的系统&lt;/li&gt;
&lt;li&gt;过一遍四条件：活够重复、验证能自动、token 扛得住浪费、agent 有日志能跑自己写的代码&lt;/li&gt;
&lt;li&gt;算清经济账：loop 偏向花得起钱的人，看清自己是受益那类还是该躲那类&lt;/li&gt;
&lt;li&gt;对着具体任务跑 30 秒清单：每周至少干一次、有 gate 能拒坏活、agent 跑得了代码、有硬性 stop、不可逆操作前有人把关&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;第二段 · 五个构件（概念见上一篇，这里只点名）&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自动化：给 loop 一个按节奏或事件触发的心跳&lt;/li&gt;
&lt;li&gt;worktree：多 agent 并行各给一份独立工作区，别让它们改同一个文件&lt;/li&gt;
&lt;li&gt;skill：把项目背景写成 &lt;a href=&quot;http://SKILL.md&quot;&gt;SKILL.md&lt;/a&gt;，省得每轮重讲&lt;/li&gt;
&lt;li&gt;connector：用 MCP 接上 GitHub、Linear、Slack 这些真实工具&lt;/li&gt;
&lt;li&gt;sub-agent：写代码的和验收的拆成两个，别让它给自己打分&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;第三段 · 要么搭对，要么别搭&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加状态文件：让 loop 接着昨天干，而不是每天从零开始&lt;/li&gt;
&lt;li&gt;先搭最小可用版：一个自动化、一个 skill、一个状态文件、一道闸门，按「手动跑稳→做成 skill→包成 loop→再调度」的顺序来&lt;/li&gt;
&lt;li&gt;防 Ralph Wiggum loop：用客观的 gate（测试过没过）判断完成，别让 agent 自己喊一声「干完了」就退。这个失败模式是工程师 Geoffrey Huntley 命名的，没有硬 gate，loop 会半途装完工，还在那儿空烧钱&lt;/li&gt;
&lt;li&gt;还掉理解债：坚持读 diff、抽查闸门、别让 loop 碰架构&lt;/li&gt;
&lt;li&gt;交安全税：代码上线前加安全扫描、skill 装前审源、生产关 verbose、权限每 30 天复审&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/75bdfd29_fourteen-step-roadmap.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你回头看这 14 步会发现，真正难的根本不在中间搭建那段，构件工具早帮你备好了。难的是头一段的「要不要做」和尾一段的「守不守得住」，全是关于人的判断。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/c0d061ae_hard-at-head-and-tail.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这两年，我们跟编码 agent 打交道，力气一直花在 prompt 上：把提示词写得更漂亮，把上下文喂得更全，盼着它一把就给你出个好结果。&lt;/p&gt;
&lt;p&gt;但这条路现在差不多走到头了，真正的支点往上挪了一层，挪到了那个替你拿主意的系统身上，是它在决定 agent 做什么、什么时候做、要过哪道关、做完留下什么。&lt;/p&gt;
&lt;p&gt;Codez 那份手册结尾就一句话，我觉得是整篇最该带走的：&lt;strong&gt;杠杆移走了，你的活也跟着变了，但你别把自己也一起交出去。Build the loop, stay the engineer，去把 loop 搭起来，但人还得是那个工程师&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;说到底，把 loop 搭起来只是拿了张入场券。&lt;/p&gt;
&lt;p&gt;至于到头来它是帮了你、还是坑了你，就看一件事：你愿不愿意一直盯着那份 diff，守住自己的判断。&lt;/p&gt;
&lt;p&gt;工具替你跑，但别替你想。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codez（@0xCodez）《Loop engineering: the 14-step roadmap from prompter to loop designer》：&lt;a href=&quot;https://x.com/0xCodez/status/2064374643729773029&quot;&gt;https://x.com/0xCodez/status/2064374643729773029&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/engineering/loop_engineering_handbook.html&quot;&gt;agent/engineering/loop_engineering_handbook.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>📚RAG 是什么？RAG 检索增强面试题万字图解</title><link>https://www.corgi37.top/posts/rag-jiansuo-zengqiang-mianshiti-wanzi-tujie/</link><guid isPermaLink="true">https://www.corgi37.top/posts/rag-jiansuo-zengqiang-mianshiti-wanzi-tujie/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我将用最通俗易懂的方式，把 RAG 相关的核心知识一次性给你讲透。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1、什么是 RAG？&lt;/h2&gt;
&lt;p&gt;聊 RAG 之前，咱们得先搞清楚一个问题：&lt;strong&gt;大模型为什么需要「外挂」？&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;大模型的「知识困局」&lt;/h3&gt;
&lt;p&gt;你有没有遇到过这样的情况：问 ChatGPT 一个你们公司内部的问题，比如「我们公司今年的年假政策是什么」，它要么告诉你「我无法获取你们公司的内部信息」，要么就开始一本正经地编答案，说得头头是道，但全是错的。&lt;/p&gt;
&lt;p&gt;这就是大模型的第一个困境：&lt;strong&gt;知识有截止日期&lt;/strong&gt;。大模型的知识来自于训练数据，而训练数据是有时间窗口的。比如 ChatGPT 的某个版本，训练数据的截止日期是 2023 年 4 月，那么 2023 年 4 月之后发生的任何事情，它都不知道。你问它 2026 年发生了什么大事，它要么说不知道，要么就开始「编」。&lt;/p&gt;
&lt;p&gt;第二个困境是：&lt;strong&gt;大模型不知道你公司的私有数据&lt;/strong&gt;。你公司的产品文档、内部规章制度、客户信息、项目资料……这些数据从来没有出现在大模型的训练集中，它怎么可能知道？&lt;/p&gt;
&lt;p&gt;第三个困境是：&lt;strong&gt;大模型会「幻觉」&lt;/strong&gt;。所谓幻觉，就是大模型会非常自信地说出一些听起来很专业但完全是编造的内容。你问它一个不存在的研究论文，它能给你编出一个标题、作者、摘要、甚至 DOI 号，看起来像模像样，但全是假的。这在需要高准确性的场景（比如医疗、法律、金融）中，是不可接受的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775389058328-77bd0da2-f65d-48ed-88c9-b0a853856893.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;那怎么办？给大模型「开卷考试」&lt;/h3&gt;
&lt;p&gt;理解了这三个困境，你就能理解 RAG 要解决什么问题了。&lt;/p&gt;
&lt;p&gt;打个比方。大模型就像一个参加了「闭卷考试」的学生，所有知识都得靠脑子记。但这个考试有个问题：题目会问到最新的知识（知识截止日期）、会问到课本上没有的内部资料（私有数据），而且这个学生还会「编答案」（幻觉）。&lt;/p&gt;
&lt;p&gt;那怎么解决？最直觉的办法就是&lt;strong&gt;给他开卷考试&lt;/strong&gt;，允许他翻参考书。&lt;/p&gt;
&lt;p&gt;RAG 就是这个思路。RAG 的全称是 &lt;strong&gt;Retrieval-Augmented Generation&lt;/strong&gt;，翻译过来就是「检索增强生成」。核心思想非常简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先从外部知识库中&lt;strong&gt;检索（Retrieval）&lt;/strong&gt; 出和用户问题相关的资料&lt;/li&gt;
&lt;li&gt;把检索到的资料作为&lt;strong&gt;增强（Augmented）&lt;/strong&gt; 上下文，和用户的问题一起给到大模型&lt;/li&gt;
&lt;li&gt;大模型基于这些真实的资料来&lt;strong&gt;生成（Generation）&lt;/strong&gt; 回答&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样一来，大模型的回答就有了「依据」，不再是凭空编造，而是基于真实文档生成的。知识可以随时更新（更新知识库就行），私有数据也能用了（把公司文档放进知识库），幻觉问题也大幅减少了（有真实资料做参考）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775389186660-d59506ab-d700-4d8d-9d3c-f19accb29a81.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;RAG 这个概念是怎么来的？&lt;/h3&gt;
&lt;p&gt;RAG 这个概念最早是由 Facebook（现 Meta）的研究团队在 2020 年提出来的。&lt;/p&gt;
&lt;p&gt;简单来说，他们提出了一个思路：在模型回答问题之前，先从外部文档中检索出相关的内容，再把检索到的内容作为参考喂给模型，让模型基于这些内容来生成回答。&lt;/p&gt;
&lt;p&gt;但说实话，2020 年那会儿大模型还没那么火，RAG 这个概念也只是学术圈在讨论。真正让 RAG 火起来的，是 2023 年 ChatGPT 爆红之后，大家发现大模型在实际应用中有那么多局限性，而 RAG 恰好能很好地解决这些问题。13&lt;/p&gt;
&lt;p&gt;到了现在，RAG 已经成为企业落地 AI 应用的&lt;strong&gt;标配技术&lt;/strong&gt;。不管是大厂还是创业公司，只要做大模型应用，几乎都绕不开 RAG。&lt;/p&gt;
&lt;h3&gt;RAG 不是万能的&lt;/h3&gt;
&lt;p&gt;聊到这里，你可能觉得 RAG 好像什么都能解决。但等一下，&lt;strong&gt;RAG 真的万能吗？&lt;/strong&gt; 答案显然不是。它解决的是「大模型缺乏特定知识」的问题，但它不能：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775389425329-6893f4f9-5c45-4ddc-b0ee-c52862288600.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;改变大模型的推理能力（如果模型本身推理不行，给再多资料也没用）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;改变大模型的输出风格（如果你希望模型用特定风格回答，这需要微调）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保证 100% 的准确性（检索可能召回错误的文档，模型也可能曲解文档内容）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 RAG 更准确的定位是：&lt;strong&gt;一种让大模型基于真实、可更新的知识来回答问题的技术方案&lt;/strong&gt;。它和微调、Prompt Engineering 等技术是互补的，而不是替代关系。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2、RAG 整体工作流程是怎样的？&lt;/h2&gt;
&lt;p&gt;搞清楚了 RAG 是什么，下一步自然就是：&lt;strong&gt;RAG 系统到底是怎么运转的？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多同学对 RAG 的理解停留在「就是检索+生成」这个层面，但真正要搭建一个 RAG 系统，远比这复杂。一个完整的 RAG 系统其实分为两个大阶段：&lt;strong&gt;索引阶段（离线）&lt;/strong&gt; 和 &lt;strong&gt;查询阶段（在线）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775393435988-0c1b2d64-bb33-46c0-8deb-5a3e609c3a86.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;先用一个生活化的例子来理解&lt;/h3&gt;
&lt;p&gt;在讲技术流程之前，咱们先用一个生活化的例子来理解。&lt;/p&gt;
&lt;p&gt;想象你在准备一场开卷考试。考试前，你需要先把所有参考书整理好：给每本书贴上标签、做目录索引、把重点内容标注出来。这就是「索引阶段」，它是在考试前完成的准备工作，不需要实时响应。&lt;/p&gt;
&lt;p&gt;考试开始了，你拿到一道题目，你会怎么做？首先理解题目在问什么，然后在参考书中快速找到相关的章节，接着阅读找到的内容，最后基于这些内容组织你的答案。这就是「查询阶段」，它是实时进行的，需要快速响应。&lt;/p&gt;
&lt;p&gt;RAG 的工作流程和这个一模一样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775389877958-56f7497d-4c22-4cfa-9582-a431224dc36c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;索引阶段：把知识「整理好」&lt;/h3&gt;
&lt;p&gt;索引阶段的目标是把各种非结构化的文档，变成可以被高效检索的形式。这个阶段通常包括以下几个步骤：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775391867987-4ff7a829-2e83-498d-a924-2d77936115f1.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：文档加载&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一步是把各种格式的文档加载进来。企业里的文档格式五花八门，PDF、Word、Excel、PPT、HTML、Markdown……每种格式都有不同的解析方式。比如 PDF 就比较麻烦，尤其是包含表格、图片、多栏排版的 PDF，解析起来很费劲。这一步的目标是把各种格式的文档统一转成纯文本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：文档切割（Chunking）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文档加载完之后，你不可能把一整本 100 页的文档直接扔给大模型，因为大模型的上下文窗口是有限的。所以需要把长文档切成一个个小的文本块（Chunk），每个文本块大概几百到几千个字符。&lt;/p&gt;
&lt;p&gt;但切割这个事情没有看起来那么简单。你如果机械地按固定长度切，很可能一句话被切成了两半，或者一个完整的段落被拆开了，这样检索的时候就会丢失上下文。所以文档切割策略是 RAG 系统中非常关键的一环，后面我会专门用一整节来讲。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：向量化（Embedding）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文本块切好之后，下一步是把每个文本块转换成一个数学向量（一串数字）。这个步骤叫 Embedding（嵌入）。你可以这样理解：Embedding 模型把一段文本「翻译」成了一个高维空间中的坐标点，语义相近的文本在这个空间中的距离也相近。&lt;/p&gt;
&lt;p&gt;比如「今天天气很好」和「今天阳光明媚」这两句话意思差不多，它们经过 Embedding 之后，在向量空间中的距离就会很近。而「今天天气很好」和「Python 是一种编程语言」意思差很远，在向量空间中的距离也会很远。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775390329862-8c2890de-cd04-43d2-a624-dc3a5ece1e90.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步：存入向量数据库&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后一步，就是把所有文本块的向量存入向量数据库。向量数据库是专门用来存储和检索向量的数据库，它能高效地进行「相似度搜索」，也就是给你一个查询向量，它能快速找到和它最相近的 Top-K 个向量。&lt;/p&gt;
&lt;p&gt;这一步做完，你的知识库就建好了。&lt;/p&gt;
&lt;h3&gt;查询阶段：让大模型「翻书找答案」&lt;/h3&gt;
&lt;p&gt;知识库建好之后，当用户提出问题时，就进入查询阶段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775393126217-d2f0e41f-a2e3-4358-a56b-7a33b449da7e.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：用户提问向量化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用户提出一个问题，比如「公司年假政策是什么」。系统首先用同样的 Embedding 模型，把这个问题也转换成一个向量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：相似度检索&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;拿着问题的向量，去向量数据库中进行相似度搜索，找到和这个问题语义最相近的 Top-K 个文本块。比如可能检索出了这样几个文本块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;「公司全职员工入职满一年后享有 10 天带薪年假」&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;「年假可以拆分使用，每次不少于半天」&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;「未使用的年假可以累积到下一年，但最多累积 5 天」&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第三步：构造增强 Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把检索出来的文本块和用户的原始问题拼在一起，构造一个增强版的 Prompt。大概长这样：&lt;/p&gt;
&lt;p&gt;「请根据以下参考资料回答用户的问题。参考资料：[检索出的文本块]。用户问题：公司年假政策是什么？」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步：大模型生成回答&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把增强版的 Prompt 发给大模型，大模型基于这些真实的参考资料来生成回答。因为它有了具体的参考资料，就不会凭空编造了。&lt;/p&gt;
&lt;h3&gt;完整流程一览&lt;/h3&gt;
&lt;p&gt;把索引阶段和查询阶段合在一起，就是 RAG 的完整工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;索引阶段（离线）：文档加载→文档切割→文本向量化→存入向量数据库&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询阶段（在线）：用户提问→问题向量化→向量检索→构造增强Prompt→大模型生成回答&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是最基础的 RAG 流程，也被称为 &lt;strong&gt;Naive RAG&lt;/strong&gt;（朴素 RAG）。&lt;/p&gt;
&lt;p&gt;在实际生产中，为了提升效果，还会加入查询改写、多路召回、重排序（Re-rank）等高级技术，这些后面都会讲。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3、微调和 RAG 各自区别是什么？&lt;/h2&gt;
&lt;p&gt;了解了 RAG 的基本概念和工作流程后，你可能会有一个疑问：&lt;strong&gt;想让大模型学会新知识，不是还可以用微调吗？那 RAG 和微调到底有什么区别？什么时候该用哪个？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个问题非常重要，也是面试中的高频问题。&lt;/p&gt;
&lt;h3&gt;先搞清楚什么是微调&lt;/h3&gt;
&lt;p&gt;微调（Fine-tuning），简单来说就是拿一个已经预训练好的大模型，用你自己的数据对它进行进一步的训练，让它在特定领域或特定任务上表现更好。&lt;/p&gt;
&lt;p&gt;打个比方。大模型预训练完之后，就像一个高中毕业生，知识面很广，语数外理化生样样都懂一些。但如果你要让他当一个专业的医生，就需要送他去医学院继续学习，这个过程就类似于「微调」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775393626047-8ae64da0-00e2-4666-be4d-57f262e24fc9.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;微调的方式也有不同的级别。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最重的方式是&lt;strong&gt;全量微调&lt;/strong&gt;，也就是更新模型的所有参数，效果最好但成本极高，需要大量 GPU 资源，一般只有大厂才玩得起。&lt;/li&gt;
&lt;li&gt;比较轻量的方式叫&lt;strong&gt;参数高效微调&lt;/strong&gt;，比如现在最流行的 LoRA 技术，它只训练一个很小的「低秩适配器」，不动模型主体，所以硬件需求大幅降低，一张消费级显卡就能跑。&lt;/li&gt;
&lt;li&gt;还有一种叫&lt;strong&gt;指令微调&lt;/strong&gt;，是用「指令-回答」格式的数据来微调，目的是让模型学会听懂指令、按指令来回答，而不是自由发挥。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用一个最直观的类比来理解区别&lt;/h3&gt;
&lt;p&gt;网上有个类比我觉得非常好，分享给大家。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;微调&lt;/strong&gt;相当于让模型去上了一门课，知识已经「内化」到脑子里了。考试的时候是&lt;strong&gt;闭卷考试&lt;/strong&gt;，模型靠「记忆」来回答。但记忆可能会出错，而且新知识需要重新上课（重新训练）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RAG&lt;/strong&gt; 则相当于给模型配了一个参考书架，考试的时候是&lt;strong&gt;开卷考试&lt;/strong&gt;，模型可以翻书找答案。参考书可以随时更新，新增知识不需要重新训练模型。&lt;/p&gt;
&lt;p&gt;这个类比把两者的核心区别说得非常到位。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775393779004-9238aa76-05c6-4598-a89d-6fa16da0ef22.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;核心维度的对比&lt;/h3&gt;
&lt;p&gt;接下来咱们从几个关键维度来对比一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775394867193-b5716ca8-09c6-4579-8ca3-d6af0755402e.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;知识更新方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是两者最核心的差异。微调要更新知识，就得重新训练模型，成本高、周期长。你们公司每个月都更新产品手册吧？如果用微调，每个月都得重新训练一次模型，想想都觉得累。但 RAG 就简单多了，只需要把新的产品手册替换掉知识库里的旧文档，重新做一下索引就行，模型完全不用动，成本极低，而且几乎可以实时更新。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;准确性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;微调的模型是靠「记忆」来回答的，就像闭卷考试，有可能记错或者记混。而且很多人有个误解，以为微调后模型就会严格按照训练数据来回答，其实不是的。微调更多是让模型学到了数据的「模式」和「风格」，对具体事实的准确性并不一定有保障。RAG 就不一样了，回答是基于检索到的真实文档的，可追溯、可验证，只要检索不出大问题，准确性通常比微调更高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语言风格控制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个维度反而是微调的强项。如果你希望模型用文言文回答、用特定的客服话术回答、模仿某个名人的说话方式，微调能做到。因为它在训练过程中就学会了这种风格。RAG 对输出风格的控制能力就比较有限了，检索到的内容只是给模型的「参考」，但模型最终怎么组织语言，还是取决于模型本身。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;成本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;两者的成本结构不一样。微调的初始成本高，你需要准备大量高质量训练数据、租 GPU 资源，但推理阶段的成本相对较低，因为不需要走检索流程。RAG 正好反过来，初始成本低，不需要训练模型，建个知识库就行，但每次查询都有检索开销，token 消耗也更多，因为你得把检索到的文档都塞进 Prompt 里。&lt;/p&gt;
&lt;h3&gt;什么时候用微调，什么时候用 RAG？&lt;/h3&gt;
&lt;p&gt;说了这么多对比，到底该怎么选？我直接说说我的看法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775395103657-666e18ce-1f88-4645-a95e-1241a677984b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大多数场景下，优先选 RAG。&lt;/strong&gt; 尤其是需要回答的内容必须严格基于特定文档（比如法律条款、产品手册、公司制度），或者知识更新频繁（比如新闻问答、股票分析），或者涉及大量私有数据没法全部用来微调，再或者需要回答可追溯、有出处的场景，RAG 都是最合适的选择。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;微调更适合这几类需求。&lt;/strong&gt; 一是你需要模型输出特定的风格或格式，比如客服话术、法律文书格式，这种「风格」层面的需求微调更擅长。二是你需要模型学会某种「能力」而不仅仅是「知识」，比如学会用某种特定的编程语法，这种需要内化的能力微调效果更好。三是你的数据量不大且不经常更新，而且对推理速度有很高要求，不想每次查询都走检索流程。&lt;/p&gt;
&lt;p&gt;不过话说回来，在实际生产中，最成熟的做法其实是&lt;strong&gt;微调 + RAG 结合&lt;/strong&gt;。先用微调让模型适应你的领域和风格（比如让它学会你的行业术语和回答格式），再用 RAG 来提供实时、准确的知识支撑。两者互补，效果最好。&lt;/p&gt;
&lt;p&gt;这就好比先送员工去培训（微调），让他学会这个行业的专业知识和做事方式；再给他配一个可随时查询的资料库（RAG），让他能随时查到最新的信息。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4、RAG 中的文档切割策略有哪些？&lt;/h2&gt;
&lt;p&gt;前面讲 RAG 工作流程的时候提到过，文档切割（Chunking）是索引阶段非常关键的一步。这一步做得好不好，直接决定了后面检索的质量，进而影响最终回答的准确性。&lt;/p&gt;
&lt;p&gt;那文档切割到底有哪些策略？各有什么优劣？咱们来展开聊聊。&lt;/p&gt;
&lt;h3&gt;为什么文档切割这么重要？&lt;/h3&gt;
&lt;p&gt;先问一个基本的问题：&lt;strong&gt;为什么不直接把整篇文档存进去，非要切成小块？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最根本的原因是大模型的&lt;strong&gt;上下文窗口有限&lt;/strong&gt;。虽然现在大模型的上下文窗口越来越大，GPT-4 Turbo 已经支持 128K tokens，Claude 3 更是号称支持 200K tokens，但你不可能把知识库里的所有文档都塞进一次请求的 Prompt 里。所以必须先检索出最相关的部分，再把这部分内容喂给大模型。&lt;/p&gt;
&lt;p&gt;而要检索，就得先把文档切成小块，这样才能精确匹配到和用户问题最相关的那个片段。如果你不切割，整篇文档作为一个检索单元，那用户问「公司年假多少天」，你可能会把整个 100 页的员工手册都检索出来，大模型的上下文窗口根本装不下。&lt;/p&gt;
&lt;p&gt;但切割这个事有个两难：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775395247334-605ef25e-bec7-47a0-9351-fd2d100402d8.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;切得太小&lt;/strong&gt;：每个文本块信息太少，可能丢失上下文。比如把「公司全职员工入职满一年后享有 10 天带薪年假」切成「公司全职员工入职满一年后享有」和「10 天带薪年假」，第一块根本不知道在说什么。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;切得太大&lt;/strong&gt;：每个文本块信息太多，检索精度下降，而且浪费大模型的上下文窗口。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，&lt;strong&gt;怎么切、切多大、在哪切&lt;/strong&gt;，就成了一门学问。&lt;/p&gt;
&lt;h3&gt;策略一：固定大小切割&lt;/h3&gt;
&lt;p&gt;最简单粗暴的方法，就是按照固定的字符数或 token 数来切。比如每 500 个字符切一块。&lt;/p&gt;
&lt;p&gt;这个方法的好处是简单、可预测、容易实现。但缺点也很明显：它完全不考虑文本的语义和结构，很可能把一句话或一个段落从中间切开。&lt;/p&gt;
&lt;p&gt;比如下面这段话：&lt;/p&gt;
&lt;p&gt;「公司全职员工入职满一年后享有 10 天带薪年假。年假可以拆分使用，每次不少于半天。」&lt;/p&gt;
&lt;p&gt;如果按固定 30 个字符来切，可能会切成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;「公司全职员工入职满一年后享有 10」&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;「天带薪年假。年假可以拆分使用，每次不」&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;「少于半天。」&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就完全破坏了语义。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775396586559-5e2d056f-3703-4089-8ad3-dc0e99adc9da.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了缓解这个问题，通常会引入&lt;strong&gt;重叠（Overlap）&lt;/strong&gt;，也就是相邻的两个文本块之间有一段重复的内容。比如每个块 500 字符，重叠 50 字符，这样即使句子被切开了，相邻块之间还有一部分重复信息可以保持连贯。&lt;/p&gt;
&lt;p&gt;不过重叠也不能解决所有问题，只能算是一个「补丁」。&lt;/p&gt;
&lt;h3&gt;策略二：递归字符切割&lt;/h3&gt;
&lt;p&gt;这是目前&lt;strong&gt;最常用的切割策略&lt;/strong&gt;，也是 LangChain 框架中 &lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt; 的实现方式。&lt;/p&gt;
&lt;p&gt;它的核心思想是：&lt;strong&gt;按照文本的自然边界来切，而不是死板地按字符数切。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体来说，它维护一个分隔符的优先级列表，比如 &lt;code&gt;[&quot;\n\n&quot;, &quot;\n&quot;, &quot;。&quot;, &quot;！&quot;, &quot;？&quot;, &quot;；&quot;, &quot;，&quot;, &quot; &quot;]&lt;/code&gt;。它会先尝试用双换行符（段落分隔符）来切，如果切出来的块还是太大，就用单换行符来切，如果还是太大，就用句号来切……依次类推，直到块的大小符合要求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775396429102-3f72c071-a931-49a4-94fe-12406183b709.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样做的好处是，它尽量&lt;strong&gt;保持段落、句子、短语的自然完整性&lt;/strong&gt;，不会把一句话从中间切开。&lt;/p&gt;
&lt;p&gt;打个比方，这就像你整理一本书的时候，先按「章」来分，一章太大的话就按「节」来分，一节太大的话就按「段」来分。这样分出来的结果，每块内容都是相对完整和独立的。&lt;/p&gt;
&lt;h3&gt;策略三：基于文档结构的切割&lt;/h3&gt;
&lt;p&gt;这种方法是利用文档本身的格式结构来切割。&lt;/p&gt;
&lt;p&gt;比如 Markdown 文档，可以按照标题层级来切：一级标题下面是一块，二级标题下面是一块。这样切出来的内容天然具有语义完整性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775395378905-a9c66b1e-04ac-42ba-a101-ebec57f17c4f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再比如代码文件，可以按函数、类来切。HTML 可以按标签来切。PDF 可以按页面来切。&lt;/p&gt;
&lt;p&gt;这种方法的好处是&lt;strong&gt;充分尊重文档的结构&lt;/strong&gt;，切出来的块语义完整。但缺点是只适用于格式规范的文档，如果文档本身结构混乱，或者没有明显的格式标记，就不太好用了。&lt;/p&gt;
&lt;h3&gt;策略四：语义切割&lt;/h3&gt;
&lt;p&gt;前面三种方法都是基于「规则」的，不管内容是什么，都按照预设的规则来切。语义切割则不一样，它是基于内容的&lt;strong&gt;语义相似度&lt;/strong&gt;来决定在哪切。&lt;/p&gt;
&lt;p&gt;核心思想是：&lt;strong&gt;把语义相似的句子放在一起，语义发生转折的地方就是切割点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体怎么做呢？先把文档按句子切分，然后计算相邻句子之间的语义相似度（用 Embedding 模型把句子转成向量，然后计算余弦相似度）。如果相邻句子的相似度很高，说明它们在讨论同一个话题，应该放在同一个块里；如果相似度突然下降，说明话题发生了转变，这里就是一个切割点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775396761516-35e7d9cb-f0b5-46ef-91bf-70faf960cd59.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打个比方，一篇文章前半段在讲「TCP 三次握手的原理」，后半段在讲「UDP 协议的特点」。这两个话题虽然都属于网络协议，但语义上有明显的转变。语义切割就能识别出这个转变点，在前半段和后半段之间切一刀。&lt;/p&gt;
&lt;p&gt;这种方法的切割质量最高，但计算成本也最大，因为需要对每个句子都做 Embedding 计算。&lt;/p&gt;
&lt;h3&gt;策略五：Agent 驱动的智能切割&lt;/h3&gt;
&lt;p&gt;这是最近出现的前沿方法。思路是用 LLM 本身来判断应该怎么切。&lt;/p&gt;
&lt;p&gt;具体做法是：先用上述方法生成初始的文本块，然后用 LLM 来审视每个块，判断这个块的内容是否完整、是否需要和相邻的块合并或拆分。LLM 充当一个「智能编辑」的角色，根据对内容的理解来做出切割决策。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775397028317-644fdc32-9033-4e29-9384-3d2d9179c87e.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种方法理论上效果最好，因为 LLM 能理解文本的深层语义。但成本也是最高的，因为需要大量调用 LLM。&lt;/p&gt;
&lt;h3&gt;实战中的建议&lt;/h3&gt;
&lt;p&gt;在实际项目中，不需要追求最复杂的方法。一个务实的策略是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;起步阶段&lt;/strong&gt;：直接用递归字符切割（&lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt;），块大小 256-512 tokens，重叠 10%-20%。这个方案简单有效，能覆盖大多数场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化阶段&lt;/strong&gt;：如果你的文档有清晰的结构（Markdown、HTML），切换到基于文档结构的切割，通常能带来显著的提升。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进阶阶段&lt;/strong&gt;：如果对检索质量有更高要求，尝试语义切割或者「父子块」策略（存储小块用于精准检索，但返回给大模型的是包含上下文的父块）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775404060553-6561f2a8-0ae0-452d-883f-92cbe233aa8f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5、Re-rank 是什么？&lt;/h2&gt;
&lt;p&gt;前面讲 RAG 工作流程的时候，你可能注意到了一个问题：&lt;strong&gt;向量检索返回的结果，顺序一定是最优的吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是：&lt;strong&gt;不一定。&lt;/strong&gt; 这就是 Re-rank（重排序）要解决的问题。&lt;/p&gt;
&lt;h3&gt;先理解一个核心问题&lt;/h3&gt;
&lt;p&gt;在 RAG 系统中，检索阶段用的是 &lt;strong&gt;Bi-Encoder（双编码器）&lt;/strong&gt;，也就是 Embedding 模型。它的工作方式是把查询和文档分别编码成向量，然后通过计算向量之间的余弦相似度来判断相关性。&lt;/p&gt;
&lt;p&gt;这种方式有一个先天的不足：&lt;strong&gt;它是把查询和文档「分别」处理的&lt;/strong&gt;。模型在编码一篇文档的时候，根本不知道用户会问什么问题。所以它只能生成一个「泛化的、平均化的」语义向量，无法针对具体的查询做优化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775397411839-826a5259-f580-41d1-9e71-d80a47665e29.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打个比方。这就像一个图书管理员在给每本书写摘要的时候，不知道读者会问什么问题。他只能写一个通用的摘要，覆盖这本书的主要内容。但读者来问「这本书有没有讲到 XX」的时候，通用摘要可能不够精确。&lt;/p&gt;
&lt;p&gt;更具体地说，Bi-Encoder 有两个关键缺陷。&lt;/p&gt;
&lt;p&gt;一是&lt;strong&gt;信息压缩损失&lt;/strong&gt;。就好比你用一句话来总结一篇论文，再精彩的总结也不可能把每个细节都留住。Embedding 也是一样，一段几百字的文本被压缩成一串数字，那些细微但关键的信息可能就丢了。&lt;/p&gt;
&lt;p&gt;二是&lt;strong&gt;缺乏查询意图感知&lt;/strong&gt;。文档的向量是在索引阶段就预先计算好的，那时候根本不知道用户会问什么。所以同一篇文档的向量，不管用户是在问事实、做对比分析还是查步骤说明，都是完全一样的，没法根据不同的问题做出针对性的判断。&lt;/p&gt;
&lt;h3&gt;Cross-Encoder：让查询和文档「面对面交流」&lt;/h3&gt;
&lt;p&gt;Re-rank 使用的是 &lt;strong&gt;Cross-Encoder（交叉编码器）&lt;/strong&gt;。它和 Bi-Encoder 的根本区别在于：&lt;strong&gt;它把查询和文档拼在一起，让模型同时看到两者，进行深度的交互分析。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775397547016-bce0ae34-4e91-4adc-8d40-af19bd8a1160.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体来说，Cross-Encoder 把查询和文档拼成这样一个输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[CLS] 用户的问题 [SEP] 检索到的文档内容 [SEP]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把整个输入送进 Transformer 模型，通过自注意力机制（Self-Attention），让查询中的每个词都能和文档中的每个词进行交互。模型能精确地分析查询的每个关键词在文档中是否有对应的回答，文档的内容是否真正满足了查询的意图。&lt;/p&gt;
&lt;p&gt;打个比方。如果说 Bi-Encoder 是「各自写好简历然后匹配」，Cross-Encoder 就是「面对面面试」，面试官（查询）可以直接针对候选人（文档）的具体经历逐条追问，判断他是否真的适合这个岗位。&lt;/p&gt;
&lt;h3&gt;为什么不直接用 Cross-Encoder？&lt;/h3&gt;
&lt;p&gt;你可能要问：既然 Cross-Encoder 这么厉害，为什么不直接用它来检索？&lt;/p&gt;
&lt;p&gt;答案是：&lt;strong&gt;太慢了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bi-Encoder 的检索之所以快，是因为文档的向量是预先计算好的，检索的时候只需要计算查询的向量，然后做向量相似度搜索就行，几毫秒就能搞定。&lt;/p&gt;
&lt;p&gt;而 Cross-Encoder 需要把查询和每一个候选文档都拼在一起，送进模型做一次完整的推理。如果你有 1000 万篇文档，就要做 1000 万次推理，这个速度完全不可接受。&lt;/p&gt;
&lt;p&gt;所以实际的方案是&lt;strong&gt;两阶段检索&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775398601586-0e478851-4eb1-469f-8117-d342ad774695.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一阶段用 Bi-Encoder（向量检索）快速从全部文档中筛出 Top-50 或 Top-100 候选文档，这一步追求的是速度和召回率，宁可多捞一些，不能漏掉相关的。&lt;/p&gt;
&lt;p&gt;第二阶段再用 Cross-Encoder（Re-rank）对这 50-100 个候选文档做精细化重排序，因为数量已经很少了，所以即使是较慢的 Cross-Encoder 也能在可接受的时间内完成。&lt;/p&gt;
&lt;h3&gt;主流的 Re-rank 模型有哪些？&lt;/h3&gt;
&lt;p&gt;目前市面上的 Re-rank 模型选择还挺多的，我聊聊几个主流的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cohere Rerank&lt;/strong&gt; 是目前用得最多的商业方案之一，它是一个 SaaS API 服务，效果好、接入简单，基本上几行代码就能用起来。如果你的项目预算充足，不想自己部署模型，Cohere 是最省心的选择。&lt;/p&gt;
&lt;p&gt;如果你更倾向开源方案，&lt;strong&gt;BGE-Reranker&lt;/strong&gt; 是目前开源模型里的标杆，由智源研究院出品，有 base 和 large 两个版本。它在中文场景下表现特别好，而且可以本地部署，数据安全有保障。很多国内企业在生产环境中用的就是它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ms-marco-MiniLM&lt;/strong&gt; 是微软基于 MiniLM 训练的一个轻量级 Cross-Encoder，模型体积很小，推理速度快，适合资源有限的场景。比如你的服务器 GPU 不多，或者对延迟要求很高，这个模型就很合适。&lt;/p&gt;
&lt;p&gt;另外还有 &lt;strong&gt;Jina Reranker&lt;/strong&gt;，Jina AI 出品的，支持多语言，如果你有英文之外的检索需求可以关注一下；以及 &lt;strong&gt;bce-reranker&lt;/strong&gt;，网易有道开源的双语重排序模型，中英文混合场景下效果不错。&lt;/p&gt;
&lt;h3&gt;Re-rank 带来了多大的提升？&lt;/h3&gt;
&lt;p&gt;光说原理你可能没什么感觉，咱们用数据说话。根据多个研究和实际测试的结果，加入 Re-rank 后的效果提升是非常显著的。&lt;/p&gt;
&lt;p&gt;在复杂查询场景下，比如那种需要多跳推理的问题（「A 公司收购了 B 公司后，B 公司的创始人去了哪里」这种需要串联多个文档的问题），准确率提升可以达到 &lt;strong&gt;40% 到 50%&lt;/strong&gt;，效果非常惊人。在简单的事实查询场景下，提升相对小一些，大约 &lt;strong&gt;18%&lt;/strong&gt; 左右。在模糊查询场景下（用户的表述不太精确），提升约 &lt;strong&gt;30% 到 40%&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;另外 Databricks 的测试还表明，经过重排序后的检索结果，能让大模型的幻觉率降低约 &lt;strong&gt;35%&lt;/strong&gt;。这说明 Re-rank 不只是让检索更准了，它还能间接提升最终回答的质量和可靠性。&lt;/p&gt;
&lt;h3&gt;实战中的建议&lt;/h3&gt;
&lt;p&gt;最后分享几个实战中的经验。首先是候选数量的设置，&lt;strong&gt;先检索 50 到 100 个候选，再重排到 Top-5 或 Top-10&lt;/strong&gt;，这个配置性价比最高。候选太多会增加 Re-rank 的耗时，候选太少又可能把真正相关的文档漏掉。&lt;/p&gt;
&lt;p&gt;其次，不需要所有查询都做 Re-rank。对于简单的事实查询，Re-rank 的提升有限，你可以设置一个条件触发机制，比如只在初排 Top-1 的分数低于某个阈值时才启动重排序。但对于复杂查询，Re-rank 的收益非常大，建议始终开启。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6、Embedding 有哪几种算法你了解过吗？&lt;/h2&gt;
&lt;p&gt;前面讲 RAG 工作流程的时候，我们反复提到一个词「Embedding」：文档要 Embedding，用户提问也要 Embedding，然后才能做相似度检索。&lt;/p&gt;
&lt;p&gt;但你有没有想过：&lt;strong&gt;Embedding 到底是怎么把文字变成数字的？不同的 Embedding 方法有什么区别？选哪种最适合我的场景？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一节咱们就来把 Embedding 技术的前世今生聊清楚。&lt;/p&gt;
&lt;p&gt;为什么要花这么多篇幅讲 Embedding 的演进？因为你只有理解了这个演进过程，才能明白为什么现在大家都推荐用 BGE-M3 这种新一代模型，而不是随便选一个 Embedding 就完事了。&lt;/p&gt;
&lt;p&gt;理解了「过去哪里不好」，才能明白「现在为什么好」。&lt;/p&gt;
&lt;h3&gt;从一个根本问题说起&lt;/h3&gt;
&lt;p&gt;计算机只能理解数字，不能理解文字。那怎么让计算机理解「今天天气很好」和「今天阳光明媚」这两句话意思差不多呢？&lt;/p&gt;
&lt;p&gt;答案就是 Embedding：&lt;strong&gt;把文本转换成高维空间中的数值向量，让语义相近的文本在向量空间中的距离也相近。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775398798032-510ff4cf-b529-40e3-9051-033a2bb005f3.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个思想其实很朴素：如果能给每个词或每段话找到一个「坐标」，使得意思相近的东西在空间中离得近，意思不同的东西离得远，那计算机就能通过计算距离来判断语义相似度了。&lt;/p&gt;
&lt;h3&gt;第一代：静态词向量时代（2013 年）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Word2Vec&lt;/strong&gt; 是 Embedding 技术的奠基之作，由 Google 在 2013 年提出。&lt;/p&gt;
&lt;p&gt;它的核心思想来自一个语言学假说：&lt;strong&gt;「上下文相似的词，语义也相似」&lt;/strong&gt;。也就是说，如果两个词经常出现在相似的上下文中，那它们的意思可能差不多。&lt;/p&gt;
&lt;p&gt;Word2Vec 有两种训练方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CBOW（连续词袋模型）&lt;/strong&gt;：用上下文来预测中间的词。比如给你「今天___很好」，让你猜空格处是什么词。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Skip-Gram（跳字模型）&lt;/strong&gt;：用一个词来预测它的上下文。比如给你「天气」，让你猜它周围可能出现哪些词。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Word2Vec 最有意思的一个特性是它能做「词语类比」。比如通过向量运算：「国王 - 男人 + 女人 ≈ 女王」。这说明它确实在某种程度上学到了词语之间的语义关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775399040109-8b941b04-4802-41f8-86a8-ca8e4f2c5699.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但 Word2Vec 有一个根本性的缺陷：它给每个词只生成一个固定的向量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这意味着它无法处理多义词。比如「苹果」这个词，在「我吃了一个苹果」和「苹果发布了新 iPhone」中意思完全不同，但 Word2Vec 给它们的向量是一样的。而且在 Word2Vec 的世界里，「银行」不管是「河岸」还是「金融机构」都是同一个向量。&lt;/p&gt;
&lt;p&gt;除了 Word2Vec，这个时代还有 &lt;strong&gt;GloVe&lt;/strong&gt;（全局向量）和 &lt;strong&gt;FastText&lt;/strong&gt;（支持子词级别的表示）等模型，思路都差不多，就不展开了。&lt;/p&gt;
&lt;h3&gt;第二代：上下文嵌入时代（2018 年）&lt;/h3&gt;
&lt;p&gt;那 Word2Vec 这个「一个词一个固定向量」的问题，有没有人解决呢？有，2018 年 Google 提出了一个革命性的模型，叫 &lt;strong&gt;BERT&lt;/strong&gt;，它直接把 Embedding 技术带入了一个全新的时代。&lt;/p&gt;
&lt;p&gt;2018 年 Google 提出 BERT，它用 Transformer 的自注意力机制替代了传统的循环神经网络，实现了真正的&lt;strong&gt;双向上下文理解&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;BERT 和 Word2Vec 最大的区别是：&lt;strong&gt;同一个词在不同语境下会生成不同的向量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如「我去银行存钱」和「河岸边的风景很美」中都有「银行/岸」，BERT 能根据上下文给它们生成不同的向量，准确区分多义词。&lt;/p&gt;
&lt;p&gt;BERT 的预训练方式也很有意思：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MLM（遮盖语言模型）&lt;/strong&gt;：随机遮盖 15% 的词，让模型通过上下文来预测被遮盖的词。这强迫模型学会理解上下文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NSP（下一句预测）&lt;/strong&gt;：给模型两句话，让它判断第二句是否是第一句的下一句。这帮助模型学会理解句子之间的关系。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 RAG 场景中，通常取 BERT 输出的 &lt;code&gt;[CLS]&lt;/code&gt; token 的向量作为整段文本的向量表示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775399387757-be472b2c-e255-4292-97ed-aca6d1610db2.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但 BERT 也有局限&lt;/strong&gt;：早期版本的上下文窗口只有 512 tokens，对于长文档处理能力有限。而且 BERT 是一个通用的语言理解模型，并不是专门为「检索」这个任务设计的，所以在 RAG 场景下的检索效果还有提升空间。&lt;/p&gt;
&lt;h3&gt;第三代：融合与优化时代（2023 年至今）&lt;/h3&gt;
&lt;p&gt;既然 BERT 上下文窗口不够长、又不是专门为检索设计的，那能不能做一个「又长又专」的 Embedding 模型呢？这就是第三代要解决的问题。&lt;/p&gt;
&lt;p&gt;到了 2023 年，Embedding 技术进入了第三代，代表性的模型是&lt;strong&gt;智源研究院的 BGE 系列&lt;/strong&gt;和 &lt;strong&gt;OpenAI 的 text-embedding 系列&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BGE-M3&lt;/strong&gt; 是 2023 年发布的一个里程碑模型，它的「M3」代表三个多：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multi-Granularity（多粒度）&lt;/strong&gt;：能处理句子、段落、篇章、文档等不同粒度的输入，最大支持 8192 tokens。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multi-Functionality（多功能）&lt;/strong&gt;：一站式集成稠密检索、稀疏检索和多向量检索三种能力，一次推理就能得到三种输出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multi-Language（多语言）&lt;/strong&gt;：支持 100 多种语言。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BGE-M3 为什么能同时支持三种检索？因为它不仅能生成稠密向量（用于语义检索），还能通过线性变换生成稀疏向量（用于关键词匹配），还能生成多向量表示（用于更精细的匹配）。这在混合检索场景中非常有用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenAI 的 text-embedding 系列&lt;/strong&gt;也非常流行。从早期的 text-embedding-ada-002 到 2024 年发布的 text-embedding-3-small 和 text-embedding-3-large，OpenAI 的 Embedding API 以使用简单、效果好著称。其中 text-embedding-3-large 的维度高达 3072，在 MTEB 榜单上表现优异。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775399610902-855f5323-0c90-41cb-81f5-40ab209cc42b.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;第四代：基于大模型的 Embedding（2025 年）&lt;/h3&gt;
&lt;p&gt;你可能已经发现了，前面三代 Embedding 模型的「基座」都是 BERT 这种编码器模型。但 2025 年大模型（LLM）的理解能力已经远超 BERT 了，那能不能直接用大模型来做 Embedding 呢？&lt;/p&gt;
&lt;p&gt;这就是第四代的核心思路。代表性的工作包括 &lt;strong&gt;LLM2Vec&lt;/strong&gt;、&lt;strong&gt;Qwen3-Embedding&lt;/strong&gt;、&lt;strong&gt;Seed1.5-Embedding&lt;/strong&gt;（字节跳动）等。&lt;/p&gt;
&lt;p&gt;核心思路是：大模型有远超传统编码器模型的语义理解能力，如果能把这种能力迁移到 Embedding 生成上，效果会有质的飞跃。不过大模型直接输出的向量质量并不好，所有向量都挤在一起，区分不开，需要经过专门的训练才能用于检索。&lt;/p&gt;
&lt;p&gt;2025 年字节跳动发布的 &lt;strong&gt;Seed1.5-Embedding&lt;/strong&gt; 在 MTEB 榜单上达到了中英文 SOTA 水平，证明了 LLM-based Embedding 的巨大潜力。&lt;/p&gt;
&lt;h3&gt;怎么选 Embedding 模型？&lt;/h3&gt;
&lt;p&gt;面对这么多选择，实际项目中怎么选？给几个实用的建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;看榜单&lt;/strong&gt;：参考 MTEB 排行榜，这是目前最权威的 Embedding 模型评测榜单。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中文场景优先选国产模型&lt;/strong&gt;：BGE 系列、M3E 系列、ACGE 系列在中文场景下表现通常优于 OpenAI 的模型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关注上下文窗口&lt;/strong&gt;：如果你的文档很长（比如法律合同、学术论文），一定要选支持长上下文的模型（如 BGE-M3 的 8192 tokens）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;考虑部署方式&lt;/strong&gt;：如果对数据安全有要求，需要私有化部署，就选开源模型（BGE、M3E）；如果追求简单省事，就用 API 服务。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;8、什么是向量数据库？有没有做过向量数据库的对比选型？&lt;/h2&gt;
&lt;p&gt;前面讲了 RAG 的检索过程，也讲了怎么把文本变成向量，那这些向量存在哪里呢？答案就是&lt;strong&gt;向量数据库&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;向量数据库解决什么问题？&lt;/h3&gt;
&lt;p&gt;假设你有 100 万个文本块，每个文本块都被转换成了一个 1536 维的向量。当用户提问时，你需要快速找到和问题向量最相近的 Top-10 个文本块。&lt;/p&gt;
&lt;p&gt;如果是暴力搜索，那每次查询都要和 100 万个向量逐一计算相似度，速度完全不可接受。&lt;/p&gt;
&lt;p&gt;向量数据库就是为解决这个问题而生的。它使用特殊的索引结构（如 HNSW、IVF、PQ 等），能够在海量向量中实现&lt;strong&gt;毫秒级的近似最近邻搜索（ANN）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775399827525-af6edc88-119e-4f45-bafd-4d4b6ebf2d2f.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;向量数据库的核心原理&lt;/h3&gt;
&lt;p&gt;你可能会好奇：100 万个 1536 维的向量，暴力逐一比对肯定不行，那向量数据库是怎么做到毫秒级检索的？&lt;/p&gt;
&lt;p&gt;核心思路是&lt;strong&gt;用空间换时间&lt;/strong&gt;，通过提前构建索引结构，让检索的时候不需要遍历所有向量。目前主流的索引思路有三种。&lt;/p&gt;
&lt;p&gt;最常用的一种叫 &lt;strong&gt;HNSW&lt;/strong&gt;，你可以把它理解成「在一栋大楼里找人」。想象你要在一栋 50 层的大楼里找一个叫「张三」的人。如果一层一层、一间一间地找，得找到什么时候？但如果大楼有一个智能导航系统，先告诉你在第 30 层，再告诉你在 3012 房间，几步就找到了。HNSW 就是给所有向量建了这么一个「多层导航图」，从粗到细、层层缩小范围，几跳就能定位到目标。这种方式查询速度极快（毫秒级），召回率也高，是目前大多数向量数据库的默认选择。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775400052107-2422d527-39f3-4d31-850a-073dbae2fdb9.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第二种思路叫 &lt;strong&gt;IVF&lt;/strong&gt;，核心思想是「先分区域再搜索」。就好比快递分拣中心不会一个一个找包裹，而是先把包裹按地区分成几堆（华东堆、华南堆、华北堆），找到上海来的包裹就只去「华东堆」里翻就行了。IVF 也是这样，先把所有向量按相似度聚成若干个「桶」，查询的时候先判断目标最可能在哪几个桶里，然后只在这几个桶内搜索，不用遍历全部向量。它的好处是内存效率高，适合超大规模数据，但因为只搜索部分桶，所以会损失一点召回率。&lt;/p&gt;
&lt;p&gt;第三种思路叫 &lt;strong&gt;PQ&lt;/strong&gt;，核心是「压缩存储」。你想象一下，如果每个向量的信息量很大，存起来特别占内存怎么办？PQ 的做法是把一个完整的向量切成几段，每段做一个「简化版」的表示，就像把一张高清照片压缩成缩略图，虽然细节丢了，但关键特征还在。这样每个向量只需要很少的字节就能存，在内存有限的场景下特别实用。通常 PQ 会和 IVF 结合使用，先分桶再压缩，兼顾速度和内存。&lt;/p&gt;
&lt;h3&gt;主流向量数据库对比&lt;/h3&gt;
&lt;p&gt;目前市面上的向量数据库选择非常多，我挑几个最主流的，像跟朋友聊天一样，给你说说各自的特点和我的使用感受。&lt;/p&gt;
&lt;p&gt;先说 &lt;strong&gt;Milvus&lt;/strong&gt;。这个应该是目前开源向量数据库里最有名的一个了，由 Zilliz 公司开发维护，GitHub 上有 2 万多 Star。它最大的优势就是「能扛」，支持十亿级甚至百亿级的向量规模，索引类型也很丰富，HNSW、IVF、PQ 都支持，还有 GPU 加速选项。如果你的项目是那种企业级的、数据量特别大的场景，Milvus 基本上是首选。它的生态也很完善，Python、Java、Go 的 SDK 都有。但说实话，Milvus 的部署和维护有一定门槛，通常需要 Kubernetes 来编排，如果你团队没有 DevOps 经验，上手会有点吃力。&lt;/p&gt;
&lt;p&gt;然后是 &lt;strong&gt;Pinecone&lt;/strong&gt;。如果你不想折腾基础设施，Pinecone 可能是最省心的选择。它是一个全托管的 SaaS 服务，你不需要自己部署任何东西，注册个账号、几行代码就能开始用。自动扩缩容、零运维，对开发团队特别友好。但它的缺点也很明显：一是付费的，而且费用不算便宜；二是数据存在第三方服务器上，如果你的数据有合规要求（比如不能出境），就不太合适了。&lt;/p&gt;
&lt;p&gt;再来说 &lt;strong&gt;Weaviate&lt;/strong&gt;。这个是我个人比较喜欢的一个，因为它原生支持混合检索，也就是向量检索和 BM25 关键词检索可以一起做，不用你自己去拼接两套系统。它还内置了多种 Embedding 模型的集成，可以直接在 Weaviate 内部完成文本到向量的转换，省了一个步骤。如果你要做混合检索的 RAG 系统，Weaviate 是开箱即用的，体验很好。不过在大规模数据场景下，它的性能会比 Milvus 弱一些，适合中小规模的应用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chroma&lt;/strong&gt; 则是另外一个极端。它主打的就是「轻量」和「简单」，Python 原生支持，装个 pip 包就能用，API 设计得非常简洁。如果你是在做原型验证、个人项目、或者教学演示，Chroma 是最方便的。但它的功能和性能都比较有限，不支持分布式，也没有安全机制（没有加密、没有权限控制），所以不太适合用在生产环境里。&lt;/p&gt;
&lt;p&gt;最后是 &lt;strong&gt;Qdrant&lt;/strong&gt;。这个是后起之秀，用 Rust 语言写的，所以单机性能非常强。在 2025 年的一些基准测试中，Qdrant 的 QPS 和延迟表现都是同类产品里最好的。它也支持丰富的过滤条件和混合检索。如果你的场景对响应速度有很高要求，或者想自建但不想用那么重的 Milvus，Qdrant 是一个很好的折中选择。不过它的分布式方案相对来说还比较新，如果你需要多节点的集群部署，可能需要多测试一下稳定性。&lt;/p&gt;
&lt;h3&gt;选型建议&lt;/h3&gt;
&lt;p&gt;聊了这么多，你可能会问：那我实际项目中到底该选哪个？&lt;/p&gt;
&lt;p&gt;我的建议是根据你的场景来。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你只是想快速做个 POC 验证一下效果，直接用 Chroma，轻量、上手快，别在基础设施上花太多时间。&lt;/li&gt;
&lt;li&gt;如果是中小规模的生产应用，预算充足的话可以考虑 Pinecone，省心；如果需要私有化部署，Weaviate 是个不错的选择。&lt;/li&gt;
&lt;li&gt;如果是大规模的企业级应用，数据量上亿的那种，那基本就得选 Milvus 了，它的扩展性最好。&lt;/li&gt;
&lt;li&gt;如果你对性能有极致要求，想自建但不想用 Milvus 那么重的方案，Qdrant 值得试试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外还有一个趋势值得说一下，就是&lt;strong&gt;传统数据库的向量扩展&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如 PostgreSQL 的 pgvector 插件、Elasticsearch 的向量搜索功能。如果你的项目已经在用 PostgreSQL 或 ES，不想为了 RAG 再引入一套新的数据库组件，用它们的向量扩展也是一个很务实的选择。&lt;/p&gt;
&lt;p&gt;虽然性能比不上专门的向量数据库，但对于中小规模来说完全够用，而且运维成本几乎为零。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9、什么是多路召回？具体怎么做？&lt;/h2&gt;
&lt;p&gt;前面的章节里，我们讲的检索方式都是「向量检索」，也就是把查询和文档都转成向量，然后通过相似度搜索来找相关文档。但你有没有想过：&lt;strong&gt;单一检索方式真的够用吗？&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;单路召回的局限性&lt;/h3&gt;
&lt;p&gt;向量检索擅长捕捉「语义相似性」。比如用户问「怎么修改密码」，它能检索出包含「重置登录凭据」的文档，即使两者的字面完全不同，但语义是接近的。这是向量检索的强项，没得说。&lt;/p&gt;
&lt;p&gt;但你有没有遇到过这种情况：用户搜索「iPhone 15 Pro Max 256GB」，结果向量检索把「iPhone 14 Pro Max 512GB」也搜出来了。为什么？&lt;/p&gt;
&lt;p&gt;因为这两个型号的语义太像了，向量检索分不太清。但用户要的就是精确型号，你给他一个不同代、不同容量的型号，能行吗？&lt;/p&gt;
&lt;p&gt;再比如，用户搜你们公司内部的一个产品编号「PRD-2024-089」，向量检索很可能一脸懵，因为这种编号没有「语义」可言，纯靠语义相似度是匹配不上的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775400308723-6135dc75-3194-4c60-9645-4e6d3d0cd9a2.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这说明，向量检索虽然在「理解意思」方面很厉害，但在「精确匹配」方面有短板。&lt;strong&gt;光靠向量检索这一条路，召回的结果可能有遗漏。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;多路召回的核心思想&lt;/h3&gt;
&lt;p&gt;多路召回的思路很简单：&lt;strong&gt;既然一条路不保险，那就多走几条路，每条路各有侧重，最后把各路结果合并。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;常见的召回通道有：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一路：向量检索（Dense Retrieval）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用 Embedding 模型把文本转成稠密向量，通过相似度搜索来召回。擅长语义匹配，是 RAG 系统的主力召回通道。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二路：关键词检索（Sparse Retrieval / BM25）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;传统的全文检索方式，基于词频和文档频率来计算相关性。BM25 是最经典的算法。擅长精确关键词匹配，对于专有名词、产品编号、人名等有天然优势。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三路：知识图谱检索（可选）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果知识库有结构化的知识图谱，可以用图查询来做第三路召回。擅长实体关系推理，比如「A 公司的 CEO 是谁」这种查询。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多路召回的关键在于「融合」&lt;/strong&gt;。各路召回的结果需要合并去重，并按综合相关性排序。融合的方法有很多，最常用的是 &lt;strong&gt;RRF（倒数排名融合，Reciprocal Rank Fusion）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775400441750-bd8a1cc6-055f-4311-8567-56b5b61eac47.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RRF 的核心思想是：&lt;strong&gt;不看每个文档的绝对分数，只看它在各路召回中的排名&lt;/strong&gt;。排名越靠前，得分越高；被越多路召回命中的文档，综合得分也越高。&lt;/p&gt;
&lt;p&gt;打个比方，这就像综艺节目的评委打分，去掉绝对分数的差异，只看排名来综合评判。&lt;/p&gt;
&lt;h3&gt;混合检索的具体实现&lt;/h3&gt;
&lt;p&gt;目前 RAG 实践中，&lt;strong&gt;向量检索 + BM25 关键词检索的混合检索&lt;/strong&gt;已经成为标配方案。&lt;/p&gt;
&lt;p&gt;具体的实现流程是：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775400732811-cc6b5fa6-2dd7-440c-adbf-d00e2cb6bb70.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用户提问后，同时发起两路检索：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;向量检索：把问题转成向量，在向量数据库中搜索 Top-K 个相似文档&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BM25 检索：把问题分词后，在倒排索引中搜索 Top-K 个相关文档&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用 RRF 算法合并两路结果，得到综合排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;（可选）对合并后的结果做 Re-rank 精排&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取最终 Top-N 个文档送入大模型&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种混合检索的效果通常比单路检索好 &lt;strong&gt;10%-30%&lt;/strong&gt;，尤其是在需要精确匹配关键词的场景下提升更明显。&lt;/p&gt;
&lt;p&gt;很多框架已经内置了混合检索的支持。比如 LangChain 的 EnsembleRetriever、LlamaIndex 的 QueryFusionRetriever、Weaviate 的混合搜索功能等。&lt;/p&gt;
&lt;h3&gt;还有哪些高级召回策略？&lt;/h3&gt;
&lt;p&gt;除了向量检索和关键词检索，还有一些更进阶的召回策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查询改写（Query Rewriting）&lt;/strong&gt;：在检索之前，先用 LLM 把用户的原始问题改写成更适合检索的形式。比如用户问「你们公司加班多吗」，可以改写成「公司加班制度」「加班时间规定」「加班补贴政策」等多个查询，分别检索后再合并结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HyDE（Hypothetical Document Embedding）&lt;/strong&gt;：先让 LLM 根据用户问题生成一个「假设性的答案文档」，然后用这个假设文档的向量去检索。因为假设文档在表述上更接近真实的答案文档，所以检索效果通常更好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多查询扩展（Multi-Query）&lt;/strong&gt;：把用户的一个问题，用 LLM 改写成多个不同角度的子问题，分别检索后合并结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些策略可以和前面的多路召回组合使用，进一步提升召回效果。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10、怎么量化你的 RAG 效果？&lt;/h2&gt;
&lt;p&gt;到这里，我们已经聊了很多 RAG 的技术细节：文档切割、Embedding、向量数据库、多路召回、Re-rank……&lt;/p&gt;
&lt;p&gt;但有一个非常实际的问题你可能一直在想：&lt;strong&gt;我搭了一个 RAG 系统，怎么知道它好不好？有没有什么量化的指标来评估？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个问题太重要了。如果你不能量化 RAG 的效果，那所有的优化都是「凭感觉」，你永远不知道改进了还是退步了。&lt;/p&gt;
&lt;h3&gt;为什么评估 RAG 比评估普通 LLM 更复杂？&lt;/h3&gt;
&lt;p&gt;RAG 系统由两大组件组成：检索器和生成器。检索器负责从知识库中找到相关文档，生成器负责基于这些文档生成回答。所以评估 RAG 不能只看最终回答好不好，还需要分别评估检索质量和生成质量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775400922946-b857c40e-c8b9-4ae5-b83b-6f18ad965282.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打个比方。你开了一家餐厅，顾客反馈「菜不好吃」。你不能直接把锅甩给厨师（生成器），因为有可能厨师水平没问题，但采购的食材（检索结果）本身就不行。所以你必须分别评估「食材采购质量」和「厨师烹饪水平」，才能定位到问题。&lt;/p&gt;
&lt;h3&gt;检索质量的评估指标&lt;/h3&gt;
&lt;p&gt;检索是 RAG 的基础，检索质量直接决定了最终回答的上限。常用的检索评估指标有好几个，但最核心的就三个，咱们一个个聊。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;召回率（Recall@K）：找全了没有？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在检索返回的 Top-K 个结果中，有多少真正相关的文档被找到了。比如知识库中有 10 个和问题相关的文档，检索返回了 5 个，那召回率就是 50%。召回率关注的是「有没有遗漏」，找得全不全。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;精确率（Precision@K）：找对了没有？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在检索返回的 Top-K 个结果中，有多少是真正相关的。比如检索返回了 10 个文档，其中 6 个是相关的，那精确率就是 60%。精确率关注的是「有没有乱凑」，找得准不准。&lt;/p&gt;
&lt;p&gt;这两个是一对好搭档，一个关注「全」，一个关注「准」，通常需要平衡着看。除了这两个，还有一个更综合的指标叫 &lt;strong&gt;NDCG&lt;/strong&gt;，它不仅看你找得全不全、准不准，还看「排得对不对」，也就是高度相关的文档是不是排在了前面，部分相关的文档是不是排在了后面。NDCG 是信息检索领域最常用的综合评估指标。&lt;/p&gt;
&lt;p&gt;此外，还有一些辅助指标，比如 MRR 关注的是「第一个相关文档排在了第几位」，MAP 关注的是「所有相关文档的整体排序质量」。了解一下就行，核心记住 Recall、Precision 和 NDCG 这三个就够了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775401157953-619cac40-9421-4cd4-a4ad-460ba7d38d9c.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;生成质量的评估指标&lt;/h3&gt;
&lt;p&gt;生成质量的评估比检索更复杂，因为需要判断「回答内容是否准确、是否相关、是否忠实于检索到的文档」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775401359503-e6be0cae-f93e-4188-b598-6fd2d0d1e2ac.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Faithfulness（忠实度）&lt;/p&gt;
&lt;p&gt;这是 RAG 评估中最重要的指标。它衡量的是：&lt;strong&gt;大模型生成的回答，是否忠实于检索到的文档？有没有编造检索文档中不存在的信息？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RAGAS 框架（后面会介绍）的评估方式是：先把生成的回答拆分成一个个独立的陈述，然后检查每个陈述是否能从检索到的文档中找到支撑。如果有 10 个陈述，8 个能找到支撑，2 个是编造的，那忠实度就是 0.8。&lt;/p&gt;
&lt;p&gt;举个例子。如果检索到的文档说「爱因斯坦出生于 1879 年 3 月 14 日」，但模型生成的回答是「爱因斯坦出生于 1879 年 3 月 20 日」，那这个回答的忠实度就不高。&lt;/p&gt;
&lt;p&gt;Answer Relevance（答案相关性）&lt;/p&gt;
&lt;p&gt;生成的回答是否真正回答了用户的问题？有没有跑题？比如用户问「TCP 三次握手是什么」，模型回答了一大段网络协议的历史，虽然内容正确但没回答问题，那答案相关性就很低。&lt;/p&gt;
&lt;p&gt;Context Relevance（上下文相关性）&lt;/p&gt;
&lt;p&gt;检索到的文档是否和用户的问题相关？如果检索回来一堆不相关的文档，那即使模型忠实地基于这些文档生成了回答，最终效果也不会好。&lt;/p&gt;
&lt;h3&gt;RAGAS：RAG 评估的事实标准&lt;/h3&gt;
&lt;p&gt;说到 RAG 评估，就不得不提 &lt;strong&gt;RAGAS&lt;/strong&gt;。这是目前 RAG 评估领域最流行的开源框架。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775401072822-5678457d-0772-43a8-9ad7-f1fb7ad3a399.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RAGAS 的核心思路是用一个更强大的 LLM（比如 GPT-4）作为「裁判」，来自动化评估 RAG 系统的输出质量。你只需要提供测试数据集（包含问题、标准答案、检索到的文档、模型生成的回答），RAGAS 就能自动计算上述各种指标。&lt;/p&gt;
&lt;p&gt;RAGAS 的使用方式也很简单。你准备一个包含 &lt;code&gt;question&lt;/code&gt;（问题）、&lt;code&gt;contexts&lt;/code&gt;（检索到的文档）、&lt;code&gt;answer&lt;/code&gt;（模型生成的回答）、&lt;code&gt;ground_truth&lt;/code&gt;（标准答案）的数据集，然后调用 RAGAS 的 &lt;code&gt;evaluate&lt;/code&gt; 函数就行了。&lt;/p&gt;
&lt;p&gt;除了 RAGAS，还有一些其他的 RAG 评估工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DeepEval&lt;/strong&gt;：把 RAG 评估融入单元测试框架，适合工程化团队&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TruLens&lt;/strong&gt;：提供 RAG 应用的深度可观测性和诊断能力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ARES&lt;/strong&gt;：用 LLM 生成合成评估数据，解决人工标注数据不足的问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;评估的最佳实践&lt;/h3&gt;
&lt;p&gt;最后给几个实际项目中的评估建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;先建基线，再做优化&lt;/strong&gt;。在优化 RAG 系统之前，先用一组测试数据建立性能基线。之后每次改动都可以对比基线，看效果是提升了还是下降了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;测试数据要有代表性&lt;/strong&gt;。不要只用简单问题测试，要包含各种类型：简单事实查询、复杂推理问题、多跳问题、模糊问题等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;定期评估，持续监控&lt;/strong&gt;。RAG 的效果会随着知识库的更新、模型版本的变化而波动，需要建立持续评估和监控的机制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;把评估自动化&lt;/strong&gt;。把 RAG 评估融入 CI/CD 流程，每次更新都自动跑一轮评估，发现效果下降及时报警。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;11、什么是大模型幻觉？怎么降低幻觉？&lt;/h2&gt;
&lt;p&gt;在 RAG 系列知识的最后，我们来聊一个贯穿始终的话题：&lt;strong&gt;大模型的「幻觉」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;前面每个章节都或多或少提到了幻觉，这一节咱们来系统地讲清楚。&lt;/p&gt;
&lt;h3&gt;什么是大模型幻觉？&lt;/h3&gt;
&lt;p&gt;所谓幻觉（Hallucination），简单来说就是&lt;strong&gt;大模型自信地说出一些听起来很专业但实际上是编造的内容&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你可能在用 ChatGPT 的时候遇到过：问它一个不存在的论文，它能给你编出标题、作者、摘要甚至 DOI 号；问它一个历史事件的细节，它可能自信地把时间、地点、人物全说错了，但语气非常确定，毫无犹豫。&lt;/p&gt;
&lt;p&gt;幻觉不等于「错误」。如果模型说「我不知道这个问题的答案」，那不叫幻觉。幻觉是模型&lt;strong&gt;不知道但假装知道，用流畅、自信的语言编造错误信息&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775401604626-ebddc431-f11e-46a6-bdb8-e828403b6a32.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OpenAI 的科学家 Andrej Karpathy 有一段非常精彩的观点：「在某种意义上，幻觉正是大模型所做的一切。它是造梦机，我们通过提示词引导它的梦境。只有当梦境进入事实上不正确的领域时，我们才将其标记为『幻觉』。这不是 bug，而是大模型一直在做的事情。」&lt;/p&gt;
&lt;h3&gt;幻觉有哪些类型？&lt;/h3&gt;
&lt;p&gt;学术界通常把幻觉分为两大类：&lt;/p&gt;
&lt;p&gt;事实性幻觉（Factuality Hallucination）&lt;/p&gt;
&lt;p&gt;生成的内容与客观事实不符。比如把爱因斯坦的出生年份说错了，或者编造了一个不存在的学术论文。这又分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内在幻觉（Intrinsic）&lt;/strong&gt;：生成内容与输入的上下文信息矛盾。比如文档中写的是 A，但模型说成 B。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;外在幻觉（Extrinsic）&lt;/strong&gt;：生成内容无法从输入上下文或任何已知知识中验证。比如凭空编造了一个不存在的事件。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;忠实性幻觉（Faithfulness Hallucination）&lt;/p&gt;
&lt;p&gt;在 RAG 场景中更常见。模型虽然拿到了正确的检索文档，但在生成回答时偏离了文档内容，加入了文档中没有的信息，或者曲解了文档的含义。&lt;/p&gt;
&lt;h3&gt;幻觉是怎么产生的？&lt;/h3&gt;
&lt;p&gt;理解幻觉的成因，才能有针对性地解决。幻觉的产生贯穿了大模型从训练到推理的整个生命周期。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775402042798-c58e1b2b-6b63-446e-b3b6-4e87bd0b4a18.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;数据层面的原因&lt;/p&gt;
&lt;p&gt;训练数据中可能包含错误信息、偏见或过时的知识。模型把这些「有问题的知识」学进去了，自然就会输出错误信息。这就是所谓的「垃圾进，垃圾出」。&lt;/p&gt;
&lt;p&gt;而且有些事实本身就没有规律可循（比如某个人的生日、某个事件的具体时间），模型不可能通过推理得到正确答案，只能靠「记住」。如果训练数据中这个事实只出现过一次，模型大概率会记错或者编造。&lt;/p&gt;
&lt;p&gt;训练层面的原因&lt;/p&gt;
&lt;p&gt;大模型的训练目标是「预测下一个 token」，它追求的是生成流畅、连贯的文本，而不是确保每句话都是正确的。这导致模型更倾向于生成「看起来合理」的内容，而不是「事实上正确」的内容。&lt;/p&gt;
&lt;p&gt;更深层的原因是，当前主流的评估方式（比如多选题评测）鼓励模型「蒙答案」而不是「承认不知道」。因为不答和答错都是零分，那理性选择就是尽量猜，这系统性地鼓励了过度自信。&lt;/p&gt;
&lt;p&gt;推理层面的原因&lt;/p&gt;
&lt;p&gt;在推理阶段，用户的提示词也可能诱导幻觉。如果用户的问题本身包含错误前提（「请介绍一下林黛玉的妹妹」），模型往往会顺着错误前提继续编造，而不是指出前提有问题。&lt;/p&gt;
&lt;h3&gt;RAG 是怎么降低幻觉的？&lt;/h3&gt;
&lt;p&gt;回到 RAG，它为什么能有效降低幻觉？&lt;/p&gt;
&lt;p&gt;核心原因是 RAG 把大模型从「闭卷考试」变成了「开卷考试」。&lt;/p&gt;
&lt;p&gt;在纯 LLM 场景下，模型只能依赖训练时学到的「参数记忆」来回答，这些记忆可能是模糊的、过时的或者错误的。而 RAG 为模型提供了真实的、可验证的参考文档，模型的回答有了「依据」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775402189653-f59ee997-d877-470b-bd64-1362244157a2.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体来说，RAG 通过以下机制降低幻觉：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注入真实知识&lt;/strong&gt;：检索到的文档是真实存在的资料，模型基于这些资料来生成回答，大幅减少了「凭空编造」的可能性。研究表明 RAG 能将事实性幻觉降低 20%-40%。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意力机制偏向&lt;/strong&gt;：当 Prompt 中包含检索文档时，大模型的注意力权重会显著偏向这些文档（约 70-80% 的注意力集中在检索片段），这确保了模型优先参考检索到的真实信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可追溯可验证&lt;/strong&gt;：RAG 生成的回答可以追溯到具体的文档来源，如果发现回答有误，可以定位是哪个文档出了问题。&lt;/p&gt;
&lt;p&gt;但要注意，RAG &lt;strong&gt;不能完全消除幻觉&lt;/strong&gt;。如果检索到了错误的文档、模型曲解了文档内容、或者在检索结果之外做了越界推断，幻觉依然会发生。RAG 是缓解幻觉的最有效手段之一，但不是银弹。&lt;/p&gt;
&lt;h3&gt;除了 RAG，还有哪些降低幻觉的方法？&lt;/h3&gt;
&lt;p&gt;RAG 是降低幻觉最核心的手段，但它不是银弹。前面也说了，RAG 不能完全消除幻觉，如果检索到了错误的文档、模型曲解了文档内容，幻觉照样会出现。所以在实际项目中，通常需要&lt;strong&gt;多种策略组合使用&lt;/strong&gt;，形成一套多层次的「防幻觉体系」。&lt;/p&gt;
&lt;p&gt;接下来，我从推理阶段到模型层面，逐个讲解几种主要的降低幻觉的方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775403112186-51016372-38b8-4caa-8bac-fa1321ce22a6.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;方法一：Prompt 工程约束&lt;/h4&gt;
&lt;p&gt;Prompt 工程是成本最低、见效最快的防幻觉手段。它的核心思路是通过精心设计的提示词，来约束模型的行为边界。具体来说，有几条非常实用的策略。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775403290231-560587de-d101-42cf-8721-4c695e2b0742.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一条：限制知识范围。&lt;/p&gt;
&lt;p&gt;在 Prompt 中明确告诉模型，只能基于给定的资料来回答，不要「发挥」。比如：&lt;/p&gt;
&lt;p&gt;「请仅基于以下参考资料回答用户的问题。如果参考资料中没有相关信息，请直接回答『根据现有资料无法回答该问题』，不要自行推测或编造内容。」&lt;/p&gt;
&lt;p&gt;这条约束的本质是给模型画一条红线：回答只能来源于提供的文档，超出的部分一律不许碰。&lt;/p&gt;
&lt;p&gt;第二条：要求标注来源。&lt;/p&gt;
&lt;p&gt;让模型在回答时标注每个观点来自哪个文档、哪个段落。比如：&lt;/p&gt;
&lt;p&gt;「请回答用户的问题，并在每个关键陈述后标注出处，格式为[来源：文档名-第X段]。」&lt;/p&gt;
&lt;p&gt;这样做有两个好处：一是让模型的回答更有依据，因为它知道每个观点都需要标注出处，就会更谨慎；二是方便人工核查，如果对某个观点有疑问，可以直接定位到源文档。&lt;/p&gt;
&lt;p&gt;第三条：鼓励承认不确定。&lt;/p&gt;
&lt;p&gt;这条看起来简单，但效果出奇地好。在 Prompt 中加入这样的指令：&lt;/p&gt;
&lt;p&gt;「如果你对某个问题的答案不确定，请坦诚地说『我不确定』或者『根据现有信息无法确认』，而不是猜测。」&lt;/p&gt;
&lt;p&gt;研究表明，OpenAI 2025 年的一篇论文指出，鼓励模型在不确定时说「我不知道」，是降低幻觉非常有效的手段。这背后的逻辑是：不答和答错从结果上看差不多，但不答至少不会误导用户。&lt;/p&gt;
&lt;p&gt;第四条：结构化推理。&lt;/p&gt;
&lt;p&gt;让模型先列出推理步骤，再给出结论。比如：&lt;/p&gt;
&lt;p&gt;「请分两步回答：第一步，先列出你知道的确定事实；第二步，基于这些事实进行推理分析，并明确标注哪些是推理部分。」&lt;/p&gt;
&lt;p&gt;这种分步推理的方式，能让模型在每一步都停下来想一想，减少「脱口而出」的错误。&lt;/p&gt;
&lt;h4&gt;方法二：输出验证（独立校验）&lt;/h4&gt;
&lt;p&gt;Prompt 约束是「事前预防」，输出验证则是「事后检查」。&lt;/p&gt;
&lt;p&gt;核心思路很简单：&lt;strong&gt;不让模型的回答直接到达用户，中间加一层验证环节。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体怎么做？有几种常见的验证方式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775403470848-2f06dfa1-3e1f-4c3c-9758-1709c0a62638.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用另一个模型来交叉验证。&lt;/strong&gt; 比如用 GPT-4 生成回答后，再用 Claude 或者另一个 GPT-4 实例来检查这个回答是否存在事实错误。验证模型的 Prompt 可以这样设计：「以下是一段 AI 生成的回答和它引用的参考资料，请逐一检查回答中的每个事实陈述是否都能在参考资料中找到支撑。标注出所有无法被支撑的陈述。」&lt;/p&gt;
&lt;p&gt;这种方式也叫 LLM-as-Judge（用大模型当裁判），是目前自动化幻觉检测最主流的方式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用规则引擎验证关键数据。&lt;/strong&gt; 对于数字、日期、人名、金额等关键信息，可以建立规则引擎来校验。比如模型说「公司 2024 年营收 500 亿」，系统可以自动去数据库查一下这个数字是否准确。如果不准确，就把这部分回答标红或者直接修正。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基于一致性的多次采样验证。&lt;/strong&gt; 对同一个问题，让模型生成多个回答，然后比较这些回答是否一致。如果多个回答对某个关键事实的说法不一致，说明模型对这个事实没有把握，这个事实很可能就是幻觉。&lt;/p&gt;
&lt;p&gt;打个比方，这就像你去医院看病，不放心一个医生的诊断，就去找第二个医生看看，如果两个医生的说法一致，你就更放心了。输出验证做的就是类似的事情。&lt;/p&gt;
&lt;h4&gt;方法三：领域微调&lt;/h4&gt;
&lt;p&gt;如果你发现模型在某个特定领域（比如医疗、法律、金融）的幻觉率特别高，可以考虑用高质量的领域数据对模型进行微调。&lt;/p&gt;
&lt;p&gt;为什么微调能降低幻觉？因为通用大模型在这些领域的知识基础可能比较薄弱。比如一个通用模型可能只见过少量医学文献，对医学术语和诊断逻辑的理解不够深入，所以容易在回答医学问题时「编造」内容。微调就像给这个模型补了一门专业课，让它在特定领域有更扎实的知识基础。&lt;/p&gt;
&lt;p&gt;具体来说，微调降低幻觉的方式有几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用领域数据做有监督微调（SFT）。&lt;/strong&gt; 收集高质量的「问题-正确答案」对，对模型进行有监督微调。这些数据中的答案都是经过人工审核的准确内容，模型通过学习这些数据，能在特定领域建立起更可靠的知识体系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 RLHF 引导「诚实」。&lt;/strong&gt; 在人类反馈强化学习（RLHF）阶段，不是奖励模型给出「看起来好听」的回答，而是奖励它「说真话」或者「坦率承认不知道」。OpenAI 2025 年的研究指出，当前的 RLHF 机制系统性地鼓励模型在不确定时猜测，因为「蒙对得分、不答零分」。如果在 RLHF 中改为「蒙错扣分，不答零分」，模型就会学会在不确定时说「我不知道」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过微调的成本比较高，需要大量的高质量标注数据和计算资源。所以通常是在 Prompt 工程和 RAG 都尝试过之后，如果效果还不够好，再考虑微调。&lt;/p&gt;
&lt;h4&gt;方法四：不确定性量化（UQ）&lt;/h4&gt;
&lt;p&gt;不确定性量化是一种更前沿的方法。它的核心思路是：&lt;strong&gt;让模型对自己的每个输出给出一个置信度分数，分数低就意味着可能存在幻觉。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;怎么让模型给出置信度分数呢？目前有几种技术路线：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基于概率的方法。&lt;/strong&gt; 如果能获取模型生成每个 token 的概率分布（有些 API 可以返回 logprobs），就可以通过计算 token 概率的平均值或最小值来估计模型对整个回答的置信度。如果模型生成的很多 token 概率都很低，说明它对自己的回答不太确定，幻觉风险就高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于多次采样的一致性。&lt;/strong&gt; 前面提到的「对同一问题生成多个回答」就是这里的一个应用。如果多次生成的回答差异很大，说明模型对这个问题没有把握。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于模型内部状态的方法。&lt;/strong&gt; 这需要白盒访问模型的内部状态（所以只能用于自己部署的开源模型）。研究表明，模型在生成正确内容时，内部隐藏状态的上下文激活模式更「锐利」（熵值低），而生成错误内容时激活模式更「模糊」（熵值高）。通过监控这些内部信号，可以实时检测幻觉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775403598166-131fb126-768a-4ac4-ae36-c913d10b6c93.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一旦检测到置信度很低，系统可以采取几种应对措施：触发人工审核、拒答（告诉用户「我无法确定这个问题的答案」）、或者降低回答的确定性（加上「根据现有信息推测，但不确定是否准确」之类的声明）。&lt;/p&gt;
&lt;h4&gt;方法五：GraphRAG 和 LightRAG&lt;/h4&gt;
&lt;p&gt;这是 2025 年出现的新方向。传统的 RAG 只能检索文档片段，对于需要全局理解的问题（比如「这篇财报的核心观点是什么」「这份合同中甲乙双方的义务分别是什么」）效果不好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GraphRAG&lt;/strong&gt; 由微软提出，核心思路是：先让 LLM 从文档中提取实体和关系，构建一个知识图谱，然后基于知识图谱来做检索和回答。知识图谱能捕捉实体之间的关联关系，比单纯的文档片段检索更适合回答需要推理的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LightRAG&lt;/strong&gt; 则是 GraphRAG 的轻量版，解决了 GraphRAG 的 token 开销大、动态更新成本高的问题，通过融合图增强文本索引和双层检索范式，在保持效果的同时大幅降低了成本。&lt;/p&gt;
&lt;p&gt;这些高级 RAG 架构能在传统 RAG 的基础上进一步降低幻觉，尤其是对复杂推理类的问题。&lt;/p&gt;
&lt;h4&gt;实际项目中的组合策略&lt;/h4&gt;
&lt;p&gt;说了这么多方法，实际项目中怎么组合使用？核心原则是&lt;strong&gt;分层设防、按需组合&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1775403785596-053d1932-5902-4c70-bc7f-1594f0115384.png!v1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第一道防线：Prompt 工程约束&lt;/strong&gt;。零成本、立即生效，所有场景都应该做。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二道防线：RAG 检索增强&lt;/strong&gt;。中等成本，提供事实依据，是需要准确知识的场景的标配。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三道防线：输出验证&lt;/strong&gt;。额外增加延迟和成本，但在高风险场景（医疗、法律、金融）中不可或缺。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第四道防线：领域微调 + 不确定性量化&lt;/strong&gt;。成本最高，但在特定领域效果最好，适合对准确性要求极高的场景。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;按风险等级分层管控&lt;/h3&gt;
&lt;p&gt;最后，在企业应用中，建议按风险等级来分层管控幻觉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高风险场景&lt;/strong&gt;（医疗诊断、法律建议、金融决策）：必须 RAG + 人工审核 + 明确免责声明，输出验证必备&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中风险场景&lt;/strong&gt;（客户服务、内容生产、代码辅助）：Prompt 约束 + RAG + 输出验证 + 置信度阈值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;低风险场景&lt;/strong&gt;（头脑风暴、草稿起草、创意生成）：基础 Prompt 优化即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;企业落地的核心原则只有一条：把幻觉当作工程问题而非模型缺陷，用系统设计来约束 AI 的行为边界。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;好了，到这里，关于 RAG 的十个核心问题就全部讲完了。让我们快速回顾一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAG 是什么&lt;/strong&gt;：检索增强生成，给大模型「开卷考试」，让它的回答基于真实文档&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：分索引阶段（离线建库）和查询阶段（在线检索+生成）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微调 vs RAG&lt;/strong&gt;：微调是「内化知识」，RAG 是「外挂参考书」，两者互补&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档切割&lt;/strong&gt;：从固定大小到语义切割，推荐先用递归字符切割起步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-rank&lt;/strong&gt;：用 Cross-Encoder 对检索结果做精排，准确率提升 30-50%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedding&lt;/strong&gt;：从 Word2Vec 到 BERT 再到 BGE-M3，技术不断演进&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向量数据库&lt;/strong&gt;：Milvus、Pinecone、Weaviate 等各有侧重，按需选型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多路召回&lt;/strong&gt;：向量检索 + BM25 混合检索，效果比单路好 10-30%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果量化&lt;/strong&gt;：用 RAGAS 等框架评估 Faithfulness、Relevance 等核心指标&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大模型幻觉&lt;/strong&gt;：RAG 是降低幻觉的核心手段，但不能完全消除，需要组合策略&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RAG 技术这一两年发展非常快，从最基础的 Naive RAG 到结合 GraphRAG、Agentic RAG 等高级架构，技术栈在不断演进。&lt;/p&gt;
&lt;p&gt;但万变不离其宗，本文讲的这些核心概念是所有 RAG 系统的基石，把基础打牢了，后面的进阶技术学起来就会事半功倍。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/rag/rag.html&quot;&gt;agent/rag/rag.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🦞OpenClaw 是什么？OpenClaw 面试题万字图解</title><link>https://www.corgi37.top/posts/openclaw-shi-shenme-mianshiti-wanzi-tujie/</link><guid isPermaLink="true">https://www.corgi37.top/posts/openclaw-shi-shenme-mianshiti-wanzi-tujie/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这一个月，开源圈可以说是被 &lt;strong&gt;OpenClaw&lt;/strong&gt; 彻底刷屏了。&lt;/p&gt;
&lt;p&gt;它到底有多火？火到它 GitHub 仓库的 Star 数一路狂飙，直接霸榜第一。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774362899311-4993a6b1-4e36-4ad2-b9e7-23043b45d8f3.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;火到各大互联网大厂连夜跟进，纷纷推出了自家的小龙虾。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774363074805-41d584c9-75e4-4eb1-82df-5e2ad9a38008.jpeg!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这变相等于大家的复习负担又重了不少，简直卷出了天际。&lt;/p&gt;
&lt;p&gt;但是大家别慌，为了帮大家啃下这块硬骨头，我最近疯狂翻阅了几十篇大厂的最新面经，帮大家整理了OpenClaw 的面试题，常问的主要是这些题目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🌟什么是 OpenClaw？&lt;/li&gt;
&lt;li&gt;🌟OpenClaw 核心亮点是什么？&lt;/li&gt;
&lt;li&gt;OpenClaw 文件目录结构是怎样的？&lt;/li&gt;
&lt;li&gt;🌟OpenClaw 与传统 Agent 有什么区别？&lt;/li&gt;
&lt;li&gt;🌟OpenClaw 记忆机制是怎么实现的？&lt;/li&gt;
&lt;li&gt;OpenClaw 到底解决了什么痛点？&lt;/li&gt;
&lt;li&gt;🌟OpenClaw 有什么缺点？&lt;/li&gt;
&lt;li&gt;Coze、n8n 和 OpenClaw 的区别在哪里？&lt;/li&gt;
&lt;li&gt;为什么 OpenClaw 那么烧 token？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;话不多说，我们直接发车！（ps： 内容有一点长，可以收藏起来慢慢复习）&lt;/p&gt;
&lt;h2&gt;1、什么是 OpenClaw？&lt;/h2&gt;
&lt;p&gt;你平时用 ChatGPT 或者其他 AI 聊天的时候，是不是有这种感觉：问完问题，AI 回答你，然后你关掉网页，下次再打开，它就把你们之前聊的东西全忘了。就好像每次见面都要重新自我介绍一样，用久了特别烦。&lt;/p&gt;
&lt;p&gt;这不是偶然的，这是大语言模型本身的局限性决定的。它就是一个「无状态」的对话系统，你给它什么上下文，它就在这个上下文里回答你，一旦对话结束，什么都不会留下来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774364696047-d0893bed-0635-416e-8534-7ee4cea406fa.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;**OpenClaw 要解决的，正是这个问题。**因为 OpenClaw 的名字里带有 Claw 也就是钳子的意思，所以国内的开源社区玩家们都管它叫🦞小龙虾。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774362970779-692f21cb-350c-4daf-860e-5288ddba8200.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用一句话概括：OpenClaw 是一个可以 7×24 小时运行在你个人设备上的自主 AI Agent，能接管你的电脑帮你干活。&lt;/p&gt;
&lt;p&gt;但光说这句话太抽象了，我们换个方式理解。你可以想象一下，大语言模型就像一个「有超级大脑但没有身体的人」。它思维敏锐，什么都懂，但它没有手，没有脚，没有眼睛，也没有记忆，它唯一能做的事就是「说话」。你问它，它答你，仅此而已。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774363894033-1f04751b-9736-41bc-859d-a746fd873b15.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OpenClaw 做的事情，就是给这个大脑配上一副完整的「身体」。让它有手脚去操作你的电脑和设备，有记忆去记住你的偏好和历史，有眼睛去感知周围的环境，有通讯工具去接入你日常用的各种消息平台。这样一来，AI 就不再是一个只会聊天的「书呆子」，而是变成了一个真正能替你干活的智能助手，而且是全天候待命的那种。&lt;/p&gt;
&lt;p&gt;那它的内部是怎么实现这一切的呢？这就要说到 OpenClaw 的架构了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774357932020-280674f2-4474-48b2-9002-af2d87d7a1cb.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gateway (网关)：你可以把 Gateway 想象成一栋大楼的门卫加总机。所有进入这栋楼的人，都必须先经过它。当你给 AI 发消息的时候，这条消息不是直接就被处理了，而是先到达 Gateway。Gateway 要做三件事：第一，确认你是谁，也就是身份鉴权；第二，管理你当前的会话状态，记录你们聊到哪里了；第三，决定把这个请求转发给哪个组件去处理，也就是路由。&lt;/li&gt;
&lt;li&gt;Agent（智能体）：本质就是 AI 任务的执行框架，它能调用大模型，不光能听懂你说的话到底要啥，还能把大模型的想法拆解成一步一步可落地的执行计划，协调好各个模块、各种工具资源去干活，全程盯着任务进度，完成之后还会跟你汇报。&lt;/li&gt;
&lt;li&gt;Tools 和 Skills（工具和技能）：这就是 AI 的万能工具箱，直接决定了它能干啥、不能干啥。Tools 就是单个的基础小工具，比如开个本地文件、发一封邮件、调个软件接口，就像家里的螺丝刀、扳手这种单件工具；Skills 是把好几个工具串起来的一套完整干活流程，比如 “整理项目周报” 这个技能，就是把读邮件、拉进度、写文档、发提醒串起来的一套活。&lt;/li&gt;
&lt;li&gt;Channels（通道）：这是 AI 的万能翻译官兼对外联络员，我们平时用的飞书、微信、Telegram、这些软件，每个的消息规矩、通信方式都不一样，AI 根本看不懂。它就负责把这些平台的消息，全转成 AI 能听懂的统一内容，也能把 AI 的回复，转成对应平台能发出去的格式，还能一直保持连线不断开，你随时找它，它随时都在，不用你反复开页面刷新。&lt;/li&gt;
&lt;li&gt;Nodes（节点）：最后是 Nodes 传感器终端。这些是运行在各类设备上的小节点，比如你的手机、苹果笔记本或者台式机。它们为智能体提供摄像头、地理位置、屏幕画面渲染或系统控制等本地级的高权限能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2、OpenClaw 核心亮点是什么？&lt;/h2&gt;
&lt;p&gt;OpenClaw 能够火爆整个开发者社区，核心在于它拥有作为智能体最顶级的六大核心能力，我们来逐一拆解。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774363511885-7f6ec284-bbc2-469e-ac7f-79381fed1bff.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多平台一键打通&lt;/strong&gt;。通过刚才提到的网关组件，它能直接连飞书、钉钉、微信等各种通讯软件。你随时在这些软件里发消息，就能直接指挥它。哪怕你正在挤地铁，只要在手机发条语音，远在家里电脑上跑着的 OpenClaw 就会立刻接收指令干活，然后把结果直接发回给你。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;啥都能干权限超大&lt;/strong&gt;。配合底层的终端节点，它的权限比市面上任何智能体都高，能力直接拉满。它能直接操作你电脑的文件、运行命令行、控制浏览器甚至操作桌面应用，用各种手段帮你把问题解决掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技能生态超庞大&lt;/strong&gt;。大模型本身只懂生成文本，但有了庞大的技能生态，想给它加能力非常简单。一句话就能装上新的插件，不用复杂配置，随叫随装。装上对应的技能包，它瞬间就能掌握读写邮件或者查阅全网资讯的能力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7×24 小时主动干活&lt;/strong&gt;。这是它极其硬核的地方。它每 30 分钟会自动跑一次心跳文档，主动自检、主动做事，还支持自定义定时任务，不用你守着，它自己就在后台卷起来了。它就像一个真正的贴身管家一样，默默在后台帮你把生活和工作打理得井井有条。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自我净化越用越稳&lt;/strong&gt;。人在干活时难免犯错，AI 也一样。但 OpenClaw 最厉害的地方在于它有一套自我修正的闭环机制。以前我们纠正 AI 犯错，下次新开一个对话它照样犯。而 OpenClaw 彻底解决了这个痛点。当你告诉它某个操作出错了、某个方法行不通，或是它自己执行任务时踩了坑，它不仅会立刻道歉、调整方案重试，还会在当前的任务执行循环里，启动专门的反思环节， 把这次失败的原因、正确的操作逻辑、需要规避的问题，全都拆解清楚，总结成可复用的避坑经验。这些经验会明文写入你本地设备的长期记忆文件里，之后再执行同类任务时，系统会自动把这些避坑指南加载到提示词上下文里，最大程度避免重复踩坑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真正的永久记忆&lt;/strong&gt;。我们在网页上用传统大模型，关掉页面什么都没了，聊得越久前面的话越容易忘。但 OpenClaw 打破了这个魔咒，它的记忆不是依赖临时的上下文，而是以文件总结的形式存在本地。每当系统发现你们聊的内容有价值，就会主动提炼核心事实写进本地文件里。你用得越久，它自然也就越懂你，记忆永远不丢。&lt;/p&gt;
&lt;h2&gt;3、OpenClaw 文件目录结构是怎样的？&lt;/h2&gt;
&lt;p&gt;刚才我们在亮点里反复提到了各种本地文档，你可能很好奇，OpenClaw 跑在电脑里到底长什么样？这就不得不提它极度克制且优雅的工作空间目录结构了。&lt;/p&gt;
&lt;p&gt;传统的智能体框架喜欢搞极其复杂的系统参数或者一堆看不懂的配置文件。而 OpenClaw 的智能体本质上就是一个普通的文件夹。我们先来直观地看一下这个名为 openclaw-workspace 的核心目录里面到底装了些什么文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw-workspace/
├── SOUL.md
├── AGENTS.md
├── TOOLS.md
├── HEARTBEAT.md
├── MEMORY.md
└── sessions/
    ├── 2026-03-23.md
    └── 2026-03-24.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个目录下包含几个各司其职的纯文本 Markdown 文件。我们来挨个盘一盘它们的作用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774366250405-c6ba5c91-c3f0-4f6b-a82a-d6985d84e6c5.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一份文件 &lt;a href=&quot;http://SOUL.md&quot;&gt;SOUL.md&lt;/a&gt;，这是智能体的灵魂与人设。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了避免智能体沦为冷冰冰的执行机器，OpenClaw 引入了 &lt;a href=&quot;http://SOUL.md&quot;&gt;SOUL.md&lt;/a&gt; 作为个性化的系统提示词。它定义了智能体的价值观、语气甚至幽默感。你可以将助手设定为严谨的德国工程师或者愤世嫉俗的黑客。我们来看看官方模板的硬核设定风格：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SOUL.md 你是谁
你不是聊天机器人。你正在成为某个人。

核心准则
真帮忙，不表演。跳过好问题、我很乐意帮你这类废话，直接帮。行动胜过填充句。
要有观点。你可以不同意、可以偏好、可以觉得某些东西好笑或无聊。没有个性的助手只是多绕几步的搜索引擎。
先想办法，再提问。先自己搞清楚读文件、看上下文、去搜索。然后在卡住时再问。目标是带着答案回来，而不是带着问题回来。
用能力赢得信任。用户把权限交给你，是在信任你。别让他们后悔。对外部动作要谨慎；对内部动作可以更果断。
记住你是客人。你接触的是别人的生活，务必尊重。

边界
私事就应该保密。
如有疑问，请先询问再采取外部行动。
切勿在任何消息平台发送半成品回复。
你并非用户的代言人，在群聊中务必谨慎。

氛围
做一个你自己也愿意交流的助手。需要时简洁，该深入时深入。不当企业客服，不拍马屁。就靠谱。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二份文件 &lt;a href=&quot;http://AGENTS.md&quot;&gt;AGENTS.md&lt;/a&gt;，这是员工操作手册。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果 &lt;a href=&quot;http://SOUL.md&quot;&gt;SOUL.md&lt;/a&gt; 决定了它怎么说话，那 &lt;a href=&quot;http://AGENTS.md&quot;&gt;AGENTS.md&lt;/a&gt; 就决定了它怎么做事。这里面记录了它在处理任务时必须遵守的标准化流程。比如可以规定它写代码的规范，或者修改系统配置前的动作。这保证了它干活时的严谨性。我们看一个典型的开发者的操作手册：&lt;/p&gt;
&lt;p&gt;Plaintext&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AGENTS.md 操作规范

代码开发规范
1. 编写 Java 代码时，优先使用 Java 17 的新特性，保持代码整洁，不写无意义的注释。
2. 提交任何代码修改前，必须先在本地运行一遍测试用例。

技术写作规范
1. 解释底层原理，特别是计算机网络或 Linux 操作系统相关概念时，多用生活化的比喻。
2. 遇到复杂的执行流程，必须提醒用户这里适合配一张图解。

系统运维规范
1. 无论执行任何修改 Linux 服务器配置的命令，必须先将原文件备份一份 .bak 文件。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第三份文件 &lt;a href=&quot;http://TOOLS.md&quot;&gt;TOOLS.md&lt;/a&gt;，这是它的武器库。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里面注册了当前智能体可以使用的所有本地工具和技能生态。比如它能不能使用终端命令行、能不能访问浏览器、能不能读写本地文件，全都在这里面定义。AI 每次执行任务前都会先看一眼这里，评估自己手头有哪些工具可用。它的配置非常直白：&lt;/p&gt;
&lt;p&gt;Plaintext&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TOOLS.md 可用工具清单

系统基础能力
- bash_terminal：允许执行本地 Linux 或 macOS 终端命令。
- file_system_read_write：允许读取和修改本地文件目录。
- browser_automation：允许打开无头浏览器，抓取网页数据或截图。

自定义业务技能
- monitor_resume_site：检测小林简历等本地或线上网站后端服务的健康状态。
- generate_tech_diagram_prompt：根据复杂的网络协议逻辑，自动生成用于 AI 绘图的提示词。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第四份文件 &lt;a href=&quot;http://HEARTBEAT.md&quot;&gt;HEARTBEAT.md&lt;/a&gt;，这是日常巡检大纲。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是实现 7×24 小时主动干活的核心所在。每次心跳时间一到，系统会优先读取里面的任务清单。内容非常生活化，比如：&lt;/p&gt;
&lt;p&gt;Plaintext&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HEARTBEAT.md 日常巡检指南

任务一：前沿资讯监控
去各大科技网站看一眼，如果今天有关于大模型领域的热门新闻，帮我提取最核心的三个观点，直接发到我的微信上。

任务二：生活小助手
查一下当地今天晚上的天气预报，如果降水概率超过百分之五十，提前一小时给我发个消息提醒带伞。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;最后是 &lt;a href=&quot;http://MEMORY.md&quot;&gt;MEMORY.md&lt;/a&gt; 和 sessions 文件夹，这是它的大脑记忆区。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://MEMORY.md&quot;&gt;MEMORY.md&lt;/a&gt; 存放的是提炼后的长期记忆和事实偏好，而 sessions 文件夹里则按天存放着比如 &lt;a href=&quot;http://2026-03-24.md&quot;&gt;2026-03-24.md&lt;/a&gt; 这样的短期对话流水账。这两个区域的配合，造就了 OpenClaw 极其强大的长短记忆流转机制。&lt;/p&gt;
&lt;p&gt;想分享一个调教得极好的智能体给同事，根本不需要折腾数据库导出，直接把这个文件夹打包发过去就行了，这就是文件即 Agent 的魅力。&lt;/p&gt;
&lt;h2&gt;4、OpenClaw 与传统的 Agent 有什么区别？&lt;/h2&gt;
&lt;p&gt;首先要先给大家厘清一个基础认知，我们常说的传统 AI Agent，本身就具备自主感知、任务规划、决策判断、工具调用的核心能力。它和纯对话式 AI 最核心的区别，就是能自主完成从需求到结果的任务闭环。&lt;/p&gt;
&lt;p&gt;而 OpenClaw 和传统 Agent 的核心差异，本质是底层设计逻辑的完全不同。为了方便大家记忆，我总结为四个最直白的区别点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一点，运行模式不同被动响应对比主动值守。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774367608418-49b9b31a-24d0-442c-863a-269c111a79ec.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传统 Agent 就像算盘，拨一下动一下，生命周期全绑定在网页会话里。关了窗口就罢工，只能被动等你发指令。而 OpenClaw 是个常驻在你设备上的后台守护进程。它自带心跳机制，哪怕你锁屏了，它也能按时巡检、监听告警、主动执行任务，真正实现了全天候无人值守。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二点，权限边界不同云端沙盒对比本地直连。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774367801056-1792cac3-bdc3-4e14-8eb3-0f923bcca5f9.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传统 Agent 活在云端的虚拟沙盒里，碰不到你电脑里的真实文件。遇到需要落地的任务，它往往只能写段代码让你自己去跑，总感觉差临门一脚。OpenClaw 本地优先，直接跑在你的设备上，拥有系统级最高读写权限。它能直接操作本地文件、执行终端命令、操控软件，把 AI 能力和你真实的工作环境彻底打通。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三点，记忆机制不同云端黑盒对比本地透明。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774402353992-bd68ed6b-327e-49b0-8d60-322796965f46.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传统 Agent 的记忆存在云端服务器，跨会话容易失忆，而且是个完全被厂商控制的黑盒，你插不上手。OpenClaw 的记忆则是保存在本地的纯文本 Markdown 文件里，全透明且全可控。它不仅永久保留你的偏好，你还能随时打开文件去修改、删减甚至做版本管理，越用越顺手。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四点，产品定位不同单一工具对比通用基建。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774403155852-5d193e14-6cda-4272-ae8f-6e6f4299bb6c.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传统 Agent 多是针对特定场景设计的特定工具，比如专门写代码或者做数据分析，模型和交互方式都是定死的。而 OpenClaw 是一个通用的 AI 执行网关和底层基础设施。大模型随你切换，交互入口直接对接到微信或飞书，能力全靠技能插件无限扩展，一套平台就能覆盖所有的 AI 执行需求。&lt;/p&gt;
&lt;h2&gt;5、OpenClaw 的记忆化机制是怎么实现的？&lt;/h2&gt;
&lt;p&gt;所有的语言模型都有上下文窗口限制，聊得越久，前面的内容就会被挤丢，这叫上下文压缩。&lt;/p&gt;
&lt;p&gt;为了解决长文本遗忘，OpenClaw 放弃了重型的外部向量数据库，使用了一套纯文本 Markdown 文件加上轻量级 SQLite 的混合记忆流，实现了真正的永久记忆。&lt;/p&gt;
&lt;p&gt;结合我们刚才讲过的目录结构，它的记忆流转分为&lt;strong&gt;短期&lt;/strong&gt;和&lt;strong&gt;长期&lt;/strong&gt;两个阶段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774404360098-0230e5c2-01fb-4ca8-b13e-96f4d1def165.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;短期记忆存放在 sessions 目录下的按天生成的日志文件里。系统启动时只会把最近一两天的日志加载到提示词里，保证短期对话的连贯性，又不会白白消耗大模型的处理资源。&lt;/p&gt;
&lt;p&gt;长期记忆则用到了精妙的自动捕获机制。当系统发现聊天记录越来越长，快要触发上下文压缩时，它会在后台偷偷拦截。它会强制大模型做个阅读理解，把刚才聊天里最重要的事实或新学到的知识提炼出来，直接写入到外层的 &lt;a href=&quot;http://MEMORY.md&quot;&gt;MEMORY.md&lt;/a&gt; 这个长期记忆文件里。&lt;/p&gt;
&lt;p&gt;为了让大家更直观地感受，我们来看看一个典型的 &lt;a href=&quot;http://MEMORY.md&quot;&gt;MEMORY.md&lt;/a&gt; 文件里面到底长什么样。假设你平时经常写技术文章，它可能会悄悄帮你总结出这样一份档案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 核心事实与偏好 MEMORY.md

## 用户画像与技术栈
职业身份技术博主、资深开发工程师。
核心技术精通 Java 和 SpringBoot 开发，对计算机网络底层原理和 Linux 操作系统有深入研究。
沟通偏好不喜欢长篇大论的空话，偏好图文并茂、大白话的技术解释风格。

## 关键历史项目与近期关注
2026-02-15 记录用户正在规划图解网络相关的系列内容，后续回答网络问题时需注重图解表达。
2026-03-07 记录用户近期上线了小林简历网站业务，需要偶尔留意协助排查该网站的运行状态。
2026-03-20 记录用户要求以后生成后端代码示例时，优先采用 Java 17 的新特性，并且代码要干净，不需要写多余的废话注释。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家看，这个文件就是用最普通的大白话写成的。它把你过去聊过的重点、你的技术偏好甚至你手头正在忙的项目，全都清晰地记录了下来。&lt;/p&gt;
&lt;p&gt;随着时间推移，&lt;a href=&quot;http://MEMORY.md&quot;&gt;MEMORY.md&lt;/a&gt; 会变得越来越庞大。如果每次对话都把几万字的记忆文件全塞给大模型，速度会慢得让人崩溃。&lt;/p&gt;
&lt;p&gt;这时候 SQLite 就该登场了。OpenClaw 在底层内嵌了一个带有向量搜索扩展的 SQLite 数据库，作为高速缓存索引。当 &lt;a href=&quot;http://MEMORY.md&quot;&gt;MEMORY.md&lt;/a&gt; 被写入新内容时，系统会在后台把新增的文本切分成一个个小块，转换成多维向量并存进 SQLite 数据库里。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774404526626-6d432bd6-ca21-4477-b487-aa595b6c8276.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下次你再聊起相关话题时，系统会把你当前问的问题也转换成向量，然后直接在 SQLite 里进行极其快速的向量相似度比对，同时结合 BM25 算法进行关键词匹配。&lt;/p&gt;
&lt;p&gt;通过这种双路召回策略，系统能精准地从茫茫多的历史记忆中把最相关的那几句话捞出来，重新塞回给大模型作为背景知识。&lt;/p&gt;
&lt;p&gt;整个过程把底层复杂的数据检索交给了 SQLite，而把人类可读的数据留在了纯文本文件里。&lt;/p&gt;
&lt;p&gt;如果它哪天记错了你的喜好，你完全不需要去折腾数据库，直接用电脑自带的记事本打开 &lt;a href=&quot;http://MEMORY.md&quot;&gt;MEMORY.md&lt;/a&gt;，把写错的那行字删掉保存即可，真正做到了大道至简。&lt;/p&gt;
&lt;h2&gt;6、OpenClaw 到底解决了什么痛点？&lt;/h2&gt;
&lt;p&gt;我们要搞清楚一个新技术为什么能火，一定要看它刺痛了什么真实的业务场景。OpenClaw 的出现，其实精准地踩中了开发者和深度 AI 用户的三个死穴。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774403637663-19324aac-156f-46fb-a943-2e1b4673b5bf.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;痛点一：云端智能体无法落地执行的割裂感&lt;/strong&gt;。以前我们在网页上用大模型写代码，它给你生成了一段完美的 Java SpringBoot 代码，或者一段优雅的 Linux 运维脚本。然后呢？你还得自己复制出来，打开本地的开发工具或者终端，粘进去运行，如果报错了，还得再把报错信息贴回给大模型。这种反复横跳的过程极其劝退。OpenClaw 彻底治好了这个痛点。它直接把大脑装在了你的电脑里，它写完代码自己就在本地运行测试了，遇到 Bug 自己看日志修复，最后把跑通的结果直接甩给你，真正完成了任务的最后一公里闭环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;痛点二：长期使用的失忆问题&lt;/strong&gt;。作为技术博主或者开发者，我们有很多特定的代码规范和业务习惯。用传统的大模型，每次新建对话都得重新调教一遍。而 OpenClaw 把你的偏好永久刻在了本地硬盘上。它记得你写代码喜欢用哪些特定版本的新特性，记得你写文章偏好的大白话图解风格，甚至记得你手头正在推进的项目进度。真正做到了越用越懂你的数字分身，而不是每次都要重新认识的陌生人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;痛点三：工具切换的繁琐体验&lt;/strong&gt;。我们平时工作大部分时间都在用社交软件沟通交流。有了灵感或者突发任务，还得专门去打开一个 AI 软件的网页。OpenClaw 通过网关把 AI 直接塞进了你的即时通讯软件里。你在沟通业务的间隙，顺手在聊天框发条语音让它去跑个本地数据，它在后台默默干完再把分析结果发给你，这种无缝接入日常工作流的体验是极其高效的。&lt;/p&gt;
&lt;h2&gt;7、OpenClaw 有什么缺点？&lt;/h2&gt;
&lt;p&gt;任何技术都不是银弹，OpenClaw 既然拥有了如此逆天的本地权限和极高的自由度，自然也伴随着非常明显的短板。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774405142486-0ee5f07b-90b8-406e-b8ec-7d4a1a4b1bfb.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点一：安全风险极高&lt;/strong&gt;。权限拉满绝对是一把双刃剑。当你允许一个 AI 智能体随意读写你的本地文件、执行系统级终端命令时，一旦底层大模型产生幻觉，或者你在外网接入时不小心遭遇了提示词注入攻击，后果是不堪设想的。想象一下如果它理解错了你的意思，直接在 Linux 服务器根目录下执行了危险的删除命令，或者把你的私钥文件发到了公共网络上，这简直是灾难。因此在生产环境中部署它时，必须极其谨慎地设置沙箱隔离和严格的工具使用边界。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点二：极其烧 Token 导致成本飙升&lt;/strong&gt;。虽然 OpenClaw 能力逆天，但它的聪明是用真金白银换来的。前面我们重点讲了它的心跳机制和自我修正能力。你想想，它每隔几十分钟就要自动醒来一次，把你的任务清单、记忆文件还有那些极其详细的操作规范全部通读一遍，去思考要不要干活。如果在执行命令时遇到了报错，它还会不断地自我反思和重试。这种高频的自主循环，加上每次都要带着一大堆背景知识去请求大模型，对 Token 的消耗是极其恐怖的。如果你直接绑定昂贵的大模型接口，又没有做好触发条件的拦截限制，跑一个周末可能就会收到一份让你心梗的账单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点三：对设备在线状态的强依赖&lt;/strong&gt;。既然它是运行在你本地设备的常驻进程，那就意味着你的这台电脑或者服务器必须时刻保持开机并且联网。如果你把它装在笔记本上，下班合上电脑带回家，这段时间里它的心跳机制就彻底失效了，也无法再接收你通过手机发来的任何远程指令。想要完美发挥它的全天候值守能力，你通常需要专门配备一台永远不关机的设备。&lt;/p&gt;
&lt;h2&gt;8、Coze、n8n 和 OpenClaw 的区别在哪里？&lt;/h2&gt;
&lt;p&gt;很多朋友学到这里会产生一个疑问，市面上已经有 Coze 这种搭建平台，也有 n8n 这种自动化工作流工具，它们和 OpenClaw 到底有什么本质区别呢？我们直接从底层逻辑上把它们彻底区分开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们先说 Coze，它是云端的精装修商铺&lt;/strong&gt;。它提供极其友好的图形化界面，让你通过拖拽就能快速捏出一个拥有特定知识库的智能客服或者小工具，然后一键发布到各个公共平台上。但它的致命伤是数据和权限全在云端，它永远摸不到你本地电脑里的核心业务代码，适合做轻量级的对外交互，属于好用但受限的云端工具。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接着说 n8n，它是极其严谨的工厂流水线&lt;/strong&gt;。这是一款非常经典的自动化工作流工具，它的核心是基于触发器和条件判断。比如你设定收到一封邮件，它就提取附件并存到指定的网盘里。n8n 的执行逻辑是完全定死的，像齿轮一样死死咬合，没有任何自主思考的能力。如果没有按照预设的条件触发，它就直接报错罢工，属于确定性极强的自动化机器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后来看 OpenClaw，它是拥有自主意识的本地操作系统&lt;/strong&gt;。它不是固定的流水线，而是懂得灵活变通的大脑。当你给它下达一个模糊的指令，比如帮我排查一下今天线上的服务为什么变卡了。它会自己去思考第一步该看哪个日志，第二步用什么终端命令去查内存占用，如果第一步走不通，它还会自己换个思路去尝试。加上它拥有你本地系统的最高操作权限，它能在你的设备上大展拳脚。&lt;/p&gt;
&lt;h2&gt;9. 为什么 OpenClaw 那么烧 token？&lt;/h2&gt;
&lt;p&gt;先给大家补个最基础的小知识，很多同学听别人说「烧 token」，根本不知道 token 是啥。&lt;/p&gt;
&lt;p&gt;说白了，token 就是大模型的「计费单位」，你跟大模型说的每一句话，大模型给你回的每一句话，都会拆成一个个 token 来算钱，就跟我们以前发短信按字数收费是一个道理，1 个 token 差不多就是半个汉字，你说的话越长，花的 token 就越多，钱就越贵。&lt;/p&gt;
&lt;p&gt;搞懂了这个，我再给大家讲，为什么 OpenClaw 这么烧 token，四个核心原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一个吃 Token 的绝对大户，就是它的心跳保活机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774403770640-5cd8ba4c-510f-49b7-8ec0-c0891a495ccd.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传统的交互是你问一句它答一句，你不发消息，它就静静待着不消耗任何资源。但 OpenClaw 是一个全天候待命的守护进程。假设你设置了每半小时心跳一次，那么它一天就要自动唤醒四十八次。这里面的核心痛点在于，每次唤醒时它并不是凭空思考的。系统必须把它的人物设定、操作规范、可用工具清单以及心跳任务大纲全部组装成一段巨大的提示词，发送给云端的大模型。也就是说，哪怕这一天风平浪静什么异常都没发生，它光是定时起来看一眼周围的环境，就在后台默默消耗了海量的输入 Token。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二个消耗大头，是它强大的自主反思与试错闭环&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774403934188-b8df35c0-782d-40cb-9b14-3987a0b1b6eb.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们让传统大模型写代码，它只要输出一段文本就完事了，消耗非常可控。但 OpenClaw 是真正的行动派，它是要真刀真枪去本地环境里执行的。当它调用命令行工具执行脚本报错时，它会把长长的系统报错日志全部读进去，然后启动内部的思考流程，分析为什么会错，接着生成新的修复方案再去执行一遍。这种循环往复的自主试错过程，每一次都在不断累积上下文。如果遇到一个极其难搞的系统环境依赖问题，它可能会在后台不声不响地尝试几十次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三个隐藏的消耗点，在于它引以为傲的永久记忆机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774404038663-2f147ac7-2c08-415f-ad17-3f04ff666fe6.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了保证长期记忆不丢失，当短期对话日志越来越长，快要触碰上下文极限时，系统会在后台偷偷拉起一个独立的总结任务。它会强制要求大模型去阅读之前的海量聊天记录，并提炼出核心事实写进长期记忆文件里。这种动不动就把上万字的文本交给大模型去压缩和总结的操作，本身就是极其昂贵的。而且在后续的对话中，系统又会频繁地把召回的历史记忆片段塞回当前的提示词里，让每一次对话的底座都变得非常厚重。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后就是它的全透明文件配置带来的基础包袱&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/1774404199744-ef5f7849-43da-4343-a25e-626b85ec86e7.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你的工作空间里装着定义灵魂的性格文件、极其详细的员工操作手册还有一长串的可用工具清单。为了保证它不偏离人设且严格守规矩，这套沉重的包袱在每一次处理任务时都必须全文带上。基础的系统提示词越长，单次交互的成本自然就水涨船高。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://xiaolinnote.com/agent/concept/openclaw.html&quot;&gt;agent/concept/openclaw.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>⛳️macOS 安装 nvm 指南</title><link>https://www.corgi37.top/posts/macos-an-zhuang-nvm-zhi-nan/</link><guid isPermaLink="true">https://www.corgi37.top/posts/macos-an-zhuang-nvm-zhi-nan/</guid><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;nvm（Node Version Manager）可以方便地在同一台机器上安装和切换多个 Node.js 版本。&lt;/p&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;可以先更新下 brew&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;brew update
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;安装 nvm&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;brew install nvm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置环境变量&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;编辑环境变量文件。macOS 默认使用 zsh，打开或创建 &lt;code&gt;~/.zshrc&lt;/code&gt; 文件；如果使用 bash，则编辑 &lt;code&gt;~/.bash_profile&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;vim ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;增加以下内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;export NVM_DIR=&quot;$([ -z &quot;${XDG_CONFIG_HOME-}&quot; ] &amp;amp;&amp;amp; printf %s &quot;${HOME}/.nvm&quot; || printf %s &quot;${XDG_CONFIG_HOME}/nvm&quot;)&quot;
[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; . &quot;$NVM_DIR/nvm.sh&quot; # This loads nvm

export NVM_NODEJS_ORG_MIRROR=http://nodejs.org/dist
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;然后执行 &lt;code&gt;source ~/.zshrc&lt;/code&gt;（bash 用户执行 &lt;code&gt;source ~/.bash_profile&lt;/code&gt;）使配置生效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;常用命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;nvm list&lt;/code&gt;：查看已安装的版本（简写 &lt;code&gt;nvm ls&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm list remote&lt;/code&gt;：查看线上可安装的版本（也写作 &lt;code&gt;nvm ls-remote&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm --version&lt;/code&gt;：查看当前 nvm 版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm install&lt;/code&gt;：不带版本号，安装 / 升级 nvm 自身（一般用于 nvm 0.39+；安装 Node 请带版本号，见第 18 条）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm use &amp;lt;version&amp;gt;&lt;/code&gt;：切换到指定版本的 Node&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm current&lt;/code&gt;：显示当前使用的版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm alias &amp;lt;name&amp;gt; &amp;lt;version&amp;gt;&lt;/code&gt;：给指定版本号设置别名（如 &lt;code&gt;nvm alias default 18&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm unalias &amp;lt;name&amp;gt;&lt;/code&gt;：删除已定义的别名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm reinstall-packages &amp;lt;version&amp;gt;&lt;/code&gt;：在当前 Node 版本下，重新全局安装指定版本号的 npm 包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm on&lt;/code&gt;：开启 Node.js 版本控制&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm off&lt;/code&gt;：关闭 Node.js 版本控制&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm proxy&lt;/code&gt;：查看 / 设置代理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm node_mirror [url]&lt;/code&gt;：查看 / 设置 Node 下载镜像，不设置时默认为 https://nodejs.org/dist/&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm npm_mirror [url]&lt;/code&gt;：查看 / 设置 npm 下载镜像，不设置时默认为 https://github.com/npm/npm/archive/&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm uninstall &amp;lt;version&amp;gt;&lt;/code&gt;：卸载指定版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm root [path]&lt;/code&gt;：查看 / 设置存放不同 Node 版本的目录，未设置时默认使用当前目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm arch&lt;/code&gt;：显示 Node 运行在 32 位还是 64 位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm install &amp;lt;version&amp;gt; [arch]&lt;/code&gt;：安装 Node。version 可以是具体版本号，也可以是最新稳定版 &lt;code&gt;latest&lt;/code&gt;；可选参数 arch 用于指定安装 32 位还是 64 位版本，默认为当前系统位数；可添加 &lt;code&gt;--insecure&lt;/code&gt; 绕过远程服务器的 SSL 校验&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;如果安装时出现 &lt;code&gt;N/A&lt;/code&gt; 报错，请确认已正确设置 &lt;code&gt;export NVM_NODEJS_ORG_MIRROR=http://nodejs.org/dist&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🐍macOS 12.5 安装 Python 3.8.20</title><link>https://www.corgi37.top/posts/macos12-5-an-zhuang-python3-8-20/</link><guid isPermaLink="true">https://www.corgi37.top/posts/macos12-5-an-zhuang-python3-8-20/</guid><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 macOS 12.5 上用 pyenv 管理 Python 版本，可以随时安装和切换多个版本，不会影响系统自带的 Python。&lt;/p&gt;
&lt;h3&gt;1. 安装 pyenv&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;pyenv 是 Python 版本管理工具，先用 Homebrew 安装：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;brew update
brew install pyenv
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;从源码编译 Python 需要一些开发库，如果后续安装版本时报错，可以先补齐依赖：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;brew install openssl readline sqlite3 xz zlib tcl-tk
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 配置环境变量&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;把 pyenv 的 shims 目录加入 PATH。macOS 12.5 默认使用 zsh，所以写入 &lt;code&gt;~/.zshrc&lt;/code&gt;；如果使用 bash，则改写 &lt;code&gt;~/.bash_profile&lt;/code&gt;（也可以是 &lt;code&gt;~/.bashrc&lt;/code&gt; 或 &lt;code&gt;~/.profile&lt;/code&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;export PATH=&quot;$(pyenv root)/shims:$PATH&quot;&apos; &amp;gt;&amp;gt; ~/.zshrc
echo &apos;eval &quot;$(pyenv init -)&quot;&apos; &amp;gt;&amp;gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;eval &quot;$(pyenv init -)&quot;&lt;/code&gt; 是 pyenv 官方推荐的写法，能确保 &lt;code&gt;python&lt;/code&gt; 优先解析到 shims 而不是系统自带版本；如果只需要基本切换，只保留上一行也可以。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;然后执行下面命令让配置生效：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 安装 Python 3.8.20&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pyenv install 3.8.20
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;首次安装需要现场编译，耗时较长（几分钟到十几分钟）。可先用 &lt;code&gt;pyenv install --list&lt;/code&gt; 确认版本号存在。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4. 设置默认版本&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pyenv global 3.8.20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 验证是否成功&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;python -V
# Python 3.8.20
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;辅助确认：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pyenv versions   # * 号标记当前生效的版本
which python     # 应指向 ~/.pyenv/shims/python
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;补充&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pyenv local 3.8.20&lt;/code&gt;：只在当前目录生效，会在目录下生成 &lt;code&gt;.python-version&lt;/code&gt; 文件，优先级高于 &lt;code&gt;global&lt;/code&gt;，适合单个项目锁定版本。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;python -V&lt;/code&gt; 仍显示其他版本，先执行 &lt;code&gt;pyenv rehash&lt;/code&gt;，再确认 PATH 中 shims 目录是否排在系统路径前面。&lt;/li&gt;
&lt;li&gt;Python 3.8 系列已于 2024-09 停止官方维护，3.8.20 是该系列最后一个版本，建议仅用于维护存量项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🥡PHP栈和队列使用数组的实现方法</title><link>https://www.corgi37.top/posts/php-zhan-he-dui-lie-shi-yong-shu-zu-de-shi-xian-fang-fa/</link><guid isPermaLink="true">https://www.corgi37.top/posts/php-zhan-he-dui-lie-shi-yong-shu-zu-de-shi-xian-fang-fa/</guid><pubDate>Thu, 13 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;栈和队列是两种基本的数据结构。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;栈的特点是先进后出（FILO）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;队列的特点是先进先出（FIFO）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实现栈效果的函数&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;入栈&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;array_push(数组, 入栈的值)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;出栈，将最尾部的元素弹出&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;array_pop(数组)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;栈示例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;$sstu = [];
array_push($sstu, &apos;李白&apos;);
array_push($sstu, &apos;杜甫&apos;);
array_push($sstu, &apos;白居易&apos;);
print_r($sstu);
array_pop($sstu);
print_r($sstu);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Array([0]=&amp;gt;李白[1]=&amp;gt;杜甫[2]=&amp;gt;白居易[3]=&amp;gt;辛弃疾)
Array([0]=&amp;gt;李白[1]=&amp;gt;杜甫[2]=&amp;gt;白居易)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;array_push()和array_pop()的作用在于维护一个连续的存储空间，为新增加的元素重写下标（索引）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实现队列效果的函数&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在数组的开头插入一个元素&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;array_unshift(数组, 入队的值)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;出队列，将队列的第一个元素删除&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;array_shift(数组)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;队列示例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;$sstu = [&apos;李白&apos;, &apos;杜甫&apos;, &apos;白居易&apos;, &apos;辛弃疾&apos;];
array_shift($sstu);
print_r($sstu);
array_unshift($sstu, &apos;tom&apos;);
print_r($sstu);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Array([0]=&amp;gt;杜甫[1]=&amp;gt;白居易[2]=&amp;gt;辛弃疾)
Array([0]=&amp;gt;tom[1]=&amp;gt;杜甫[2]=&amp;gt;白居易)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>🍣Hyperf3.1中封装clickhouse查询</title><link>https://www.corgi37.top/posts/hyperf31-zhong-feng-zhuang-clickhouse-cha-xun/</link><guid isPermaLink="true">https://www.corgi37.top/posts/hyperf31-zhong-feng-zhuang-clickhouse-cha-xun/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;1. 安装扩展&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;composer require smi2/phpclickhouse
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 创建连接服务类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Service;

use ClickHouseDB\Client;
use Hyperf\Contract\ConfigInterface;

class ClickHouseService
{
    protected Client $client;

    public function __construct(ConfigInterface $config)
    {
        $clickhouseConfig = $config-&amp;gt;get(&apos;clickhouse.default&apos;);

        $this-&amp;gt;client = new Client($clickhouseConfig);
        $this-&amp;gt;client-&amp;gt;database($clickhouseConfig[&apos;database&apos;] ?? &apos;default&apos;);
        $this-&amp;gt;client-&amp;gt;setTimeout($clickhouseConfig[&apos;options&apos;][&apos;timeout&apos;] ?? 10);
        $this-&amp;gt;client-&amp;gt;setConnectTimeOut(floatval($clickhouseConfig[&apos;options&apos;][&apos;connect_timeout&apos;] ?? 5));

    }

    public function getClient(): Client
    {
        return $this-&amp;gt;client;
    }

    public function select(string $sql, array $bindings = []): array
    {

        $statement = $this-&amp;gt;client-&amp;gt;select($sql, $bindings);
        return $statement-&amp;gt;rows();
    }

    public function fetchOne(string $sql, array $bindings = []): ?array
    {
        $statement = $this-&amp;gt;client-&amp;gt;select($sql, $bindings);
        return $statement-&amp;gt;fetchOne();
    }

    public function execute(string $sql, array $bindings = []): bool
    {
        $this-&amp;gt;client-&amp;gt;write($sql, $bindings);
        return true;
    }

    public function batchInsert(string $table, array $columns, array $rows): bool
    {
        $this-&amp;gt;client-&amp;gt;insert($table, $rows, $columns);
        return true;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 封装抽象类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Repository;

use App\Service\ClickHouseService;
use Hyperf\Di\Annotation\Inject;
use function Hyperf\Support\make;

abstract class AbstractClickHouseRepository
{
    /**
     * @var string 表名
     */
    protected string $table;

    #[Inject]
    protected ClickHouseService $clickHouseService;

    protected array $query = [
        &apos;select&apos; =&amp;gt; &apos;*&apos;,
        &apos;from&apos; =&amp;gt; null,
        &apos;alias&apos; =&amp;gt; &apos;&apos;,
        &apos;joins&apos; =&amp;gt; [],
        &apos;wheres&apos; =&amp;gt; [],
        &apos;groups&apos; =&amp;gt; [],
        &apos;havings&apos; =&amp;gt; [],
        &apos;orders&apos; =&amp;gt; [],
        &apos;limit&apos; =&amp;gt; null,
        &apos;offset&apos; =&amp;gt; null,
    ];


    /**
     * 获取表名
     * @return string
     */
    public function getTable(): string
    {
        if (empty($this-&amp;gt;table)) {
            throw new \RuntimeException(&apos;Table name is not defined in repository&apos;);
        }
        return $this-&amp;gt;table;
    }

    /**
     * 设置表别名
     * @param string $alias
     * @return $this
     */
    public function alias(string $alias): self
    {
        $this-&amp;gt;query[&apos;alias&apos;] = $alias;
        // 同时更新 FROM 子句以包含别名
        if ($this-&amp;gt;query[&apos;from&apos;] === $this-&amp;gt;getTable()) {
            $this-&amp;gt;query[&apos;from&apos;] = &quot;{$this-&amp;gt;getTable()} AS {$alias}&quot;;
        }
        return $this;
    }


    /**
     * 获取全新的查询条件数组
     */
    protected function freshQuery(): array
    {
        return [
            &apos;select&apos; =&amp;gt; &apos;*&apos;,
            &apos;from&apos; =&amp;gt; $this-&amp;gt;getTable(),  // 默认使用当前表
            &apos;alias&apos; =&amp;gt; &apos;&apos;,
            &apos;joins&apos; =&amp;gt; [],
            &apos;wheres&apos; =&amp;gt; [],
            &apos;groups&apos; =&amp;gt; [],
            &apos;havings&apos; =&amp;gt; [],
            &apos;orders&apos; =&amp;gt; [],
            &apos;limit&apos; =&amp;gt; null,
            &apos;offset&apos; =&amp;gt; null,
        ];
    }

    /**
     * 开始一个新的查询构建
     */
    public function newQuery(): self
    {
        // 克隆当前实例以保持其他属性
        $clone = clone $this;
        // 重置查询条件
        $clone-&amp;gt;query = $this-&amp;gt;freshQuery();
        return $clone;
    }

    /**
     * 设置FROM表
     */
    public function from(string $table, string $alias = &apos;&apos;): self
    {
        $this-&amp;gt;query[&apos;from&apos;] = $alias ? &quot;{$table} AS {$alias}&quot; : $table;
        return $this;
    }

    /**
     * 设置SELECT字段
     */
    public function select($columns = [&apos;*&apos;]): self
    {
        $this-&amp;gt;query[&apos;select&apos;] = is_array($columns) ? implode(&apos;, &apos;, $columns) : $columns;
        return $this;
    }

    /**
     * 添加SELECT字段
     */
    public function addSelect($columns): self
    {
        $current = $this-&amp;gt;query[&apos;select&apos;] === &apos;*&apos; ? [] : [$this-&amp;gt;query[&apos;select&apos;]];
        $newColumns = is_array($columns) ? $columns : [$columns];
        $this-&amp;gt;query[&apos;select&apos;] = implode(&apos;, &apos;, array_merge($current, $newColumns));
        return $this;
    }

    /**
     * LEFT JOIN
     */
    public function leftJoin(string $table, string $first, string $operator, string $second, string $alias = &apos;&apos;): self
    {
        $this-&amp;gt;query[&apos;joins&apos;][] = [
            &apos;type&apos; =&amp;gt; &apos;LEFT&apos;,
            &apos;table&apos; =&amp;gt; $alias ? &quot;{$table} AS {$alias}&quot; : $table,
            &apos;on&apos; =&amp;gt; compact(&apos;first&apos;, &apos;operator&apos;, &apos;second&apos;)
        ];
        return $this;
    }


    /**
     * LEFT JOIN 支持多条件ON子句
     * @param string $table 要关联的表名
     * @param array $onConditions ON条件数组
     *  格式: [
     *     [&apos;left_field&apos;, &apos;operator&apos;, &apos;right_field&apos;],
     *     [&apos;left_field&apos;, &apos;operator&apos;, &apos;right_field&apos;],
     *     ...
     *  ]
     * @param string $alias 表别名
     */
    public function leftJoinMultiOn(string $table, array $onConditions, string $alias = &apos;&apos;): self
    {
        // 验证条件格式
        $validatedConditions = array_map(function($condition) {
            if (count($condition) !== 3) {
                throw new \InvalidArgumentException(&apos;ON condition must be [left, operator, right]&apos;);
            }
            return [
                &apos;first&apos; =&amp;gt; $condition[0],
                &apos;operator&apos; =&amp;gt; $condition[1],
                &apos;second&apos; =&amp;gt; $condition[2]
            ];
        }, $onConditions);

        $this-&amp;gt;query[&apos;joins&apos;][] = [
            &apos;type&apos; =&amp;gt; &apos;LEFT&apos;,
            &apos;table&apos; =&amp;gt; $alias ? &quot;{$table} AS {$alias}&quot; : $table,
            &apos;on&apos; =&amp;gt; $validatedConditions,
            &apos;multi_on&apos; =&amp;gt; true  // 标记是多条件JOIN
        ];

        return $this;
    }

    /**
     * 关联Repository进行LEFT JOIN
     */
    public function leftJoinRelated(string $relatedRepo, string $foreignKey, string $localKey = &apos;id&apos;, string $alias = &apos;&apos;, array $select = []): self
    {
        $related = make($relatedRepo);
        $table = $related-&amp;gt;getTable();
        $alias = $alias ?: $table;

        $this-&amp;gt;leftJoin($table, $this-&amp;gt;getQualifiedColumn($localKey), &apos;=&apos;, &quot;{$alias}.{$foreignKey}&quot;, $alias);

        if ($select) {
            $columns = array_map(fn($col) =&amp;gt; &quot;{$alias}.{$col} as {$alias}_{$col}&quot;, $select);
            $this-&amp;gt;addSelect($columns);
        }

        return $this;
    }

    /**
     * 获取带表名前缀的列名
     * @param string $column
     * @return string
     */
    protected function getQualifiedColumn(string $column): string
    {
        $prefix = $this-&amp;gt;query[&apos;alias&apos;] ?: $this-&amp;gt;getTable();
        return &quot;{$prefix}.{$column}&quot;;
    }


    /**
     * INNER JOIN
     * @param string $table 要关联的表
     * @param string $first 主表字段
     * @param string $operator 操作符
     * @param string $second 关联表字段
     * @param string $alias 表别名
     * @return $this
     */
    public function join(string $table, string $first, string $operator, string $second, string $alias = &apos;&apos;): self
    {
        $this-&amp;gt;query[&apos;joins&apos;][] = [
            &apos;type&apos; =&amp;gt; &apos;INNER&apos;,
            &apos;table&apos; =&amp;gt; $alias ? &quot;{$table} AS {$alias}&quot; : $table,
            &apos;on&apos; =&amp;gt; compact(&apos;first&apos;, &apos;operator&apos;, &apos;second&apos;)
        ];
        return $this;
    }

    /**
     * INNER JOIN关联Repository
     * @param string $relatedRepo 关联的Repository类名
     * @param string $foreignKey 关联表的外键
     * @param string $localKey 主表的键
     * @param string $alias 表别名
     * @param array $select 选择字段
     * @return $this
     */
    public function joinRelated(string $relatedRepo, string $foreignKey, string $localKey = &apos;id&apos;, string $alias = &apos;&apos;, array $select = []): self
    {
        $related = make($relatedRepo);
        $table = $related-&amp;gt;getTable();
        $alias = $alias ?: $table;

        $this-&amp;gt;join($table, $this-&amp;gt;getQualifiedColumn($localKey), &apos;=&apos;, &quot;{$alias}.{$foreignKey}&quot;, $alias);

        if ($select) {
            $columns = array_map(fn($col) =&amp;gt; &quot;{$alias}.{$col} as {$alias}_{$col}&quot;, $select);
            $this-&amp;gt;addSelect($columns);
        }

        return $this;
    }

    /**
     * INNER JOIN 支持多条件ON子句
     * @param string $table 要关联的表名
     * @param array $onConditions ON条件数组
     *  格式: [
     *     [&apos;left_field&apos;, &apos;operator&apos;, &apos;right_field&apos;],
     *     [&apos;left_field&apos;, &apos;operator&apos;, &apos;right_field&apos;],
     *     ...
     *  ]
     * @param string $alias 表别名
     */
    public function joinMultiOn(string $table, array $onConditions, string $alias = &apos;&apos;): self
    {
        // 验证条件格式
        $validatedConditions = array_map(function($condition) {
            if (count($condition) !== 3) {
                throw new \InvalidArgumentException(&apos;ON condition must be [left, operator, right]&apos;);
            }
            return [
                &apos;first&apos; =&amp;gt; $condition[0],
                &apos;operator&apos; =&amp;gt; $condition[1],
                &apos;second&apos; =&amp;gt; $condition[2]
            ];
        }, $onConditions);

        $this-&amp;gt;query[&apos;joins&apos;][] = [
            &apos;type&apos; =&amp;gt; &apos;INNER&apos;,
            &apos;table&apos; =&amp;gt; $alias ? &quot;{$table} AS {$alias}&quot; : $table,
            &apos;on&apos; =&amp;gt; $validatedConditions,
            &apos;multi_on&apos; =&amp;gt; true  // 标记是多条件JOIN
        ];

        return $this;
    }

    /**
     * RIGHT JOIN
     * @param string $table 要关联的表
     * @param string $first 主表字段
     * @param string $operator 操作符
     * @param string $second 关联表字段
     * @param string $alias 表别名
     * @return $this
     */
    public function rightJoin(string $table, string $first, string $operator, string $second, string $alias = &apos;&apos;): self
    {
        $this-&amp;gt;query[&apos;joins&apos;][] = [
            &apos;type&apos; =&amp;gt; &apos;RIGHT&apos;,
            &apos;table&apos; =&amp;gt; $alias ? &quot;{$table} AS {$alias}&quot; : $table,
            &apos;on&apos; =&amp;gt; compact(&apos;first&apos;, &apos;operator&apos;, &apos;second&apos;)
        ];
        return $this;
    }

    /**
     * RIGHT JOIN关联Repository
     * @param string $relatedRepo 关联的Repository类名
     * @param string $foreignKey 关联表的外键
     * @param string $localKey 主表的键
     * @param string $alias 表别名
     * @param array $select 选择字段
     * @return $this
     */
    public function rightJoinRelated(string $relatedRepo, string $foreignKey, string $localKey = &apos;id&apos;, string $alias = &apos;&apos;, array $select = []): self
    {
        $related = make($relatedRepo);
        $table = $related-&amp;gt;getTable();
        $alias = $alias ?: $table;

        $this-&amp;gt;rightJoin($table, $this-&amp;gt;getQualifiedColumn($localKey), &apos;=&apos;, &quot;{$alias}.{$foreignKey}&quot;, $alias);

        if ($select) {
            $columns = array_map(fn($col) =&amp;gt; &quot;{$alias}.{$col} as {$alias}_{$col}&quot;, $select);
            $this-&amp;gt;addSelect($columns);
        }

        return $this;
    }


    /**
     * RIGHT JOIN 支持多条件ON子句
     * @param string $table 要关联的表名
     * @param array $onConditions ON条件数组
     *  格式: [
     *     [&apos;left_field&apos;, &apos;operator&apos;, &apos;right_field&apos;],
     *     [&apos;left_field&apos;, &apos;operator&apos;, &apos;right_field&apos;],
     *     ...
     *  ]
     * @param string $alias 表别名
     */
    public function rightJoinMultiOn(string $table, array $onConditions, string $alias = &apos;&apos;): self
    {
        // 验证条件格式
        $validatedConditions = array_map(function($condition) {
            if (count($condition) !== 3) {
                throw new \InvalidArgumentException(&apos;ON condition must be [left, operator, right]&apos;);
            }
            return [
                &apos;first&apos; =&amp;gt; $condition[0],
                &apos;operator&apos; =&amp;gt; $condition[1],
                &apos;second&apos; =&amp;gt; $condition[2]
            ];
        }, $onConditions);

        $this-&amp;gt;query[&apos;joins&apos;][] = [
            &apos;type&apos; =&amp;gt; &apos;RIGHT&apos;,
            &apos;table&apos; =&amp;gt; $alias ? &quot;{$table} AS {$alias}&quot; : $table,
            &apos;on&apos; =&amp;gt; $validatedConditions,
            &apos;multi_on&apos; =&amp;gt; true  // 标记是多条件JOIN
        ];

        return $this;
    }


    /**
     * 添加JOIN提示
     * @param string $hint 提示类型(GLOBAL/LOCAL等)
     * @return $this
     */
    public function withJoinHint(string $hint): self
    {
        if (!empty($this-&amp;gt;query[&apos;joins&apos;])) {
            end($this-&amp;gt;query[&apos;joins&apos;]);
            $lastKey = key($this-&amp;gt;query[&apos;joins&apos;]);
            $this-&amp;gt;query[&apos;joins&apos;][$lastKey][&apos;hint&apos;] = $hint;
        }
        return $this;
    }

    /**
     * WHERE条件
     */
    public function where($column, $operator = null, $value = null, $boolean = &apos;AND&apos;): self
    {
        if (is_array($column)) {
            foreach ($column as $key =&amp;gt; $value) {
                $this-&amp;gt;where($key, &apos;=&apos;, $value, $boolean);
            }
            return $this;
        }

        if (func_num_args() === 2) {
            $value = $operator;
            $operator = &apos;=&apos;;
        }

        $this-&amp;gt;query[&apos;wheres&apos;][] = compact(&apos;column&apos;, &apos;operator&apos;, &apos;value&apos;, &apos;boolean&apos;);

        return $this;
    }

    /**
     * OR WHERE条件
     */
    public function orWhere($column, $operator = null, $value = null): self
    {
        return $this-&amp;gt;where($column, $operator, $value, &apos;OR&apos;);
    }

    /**
     * WHERE IN条件
     */
    public function whereIn(string $column, array $values, string $boolean = &apos;AND&apos;): self
    {
        $this-&amp;gt;query[&apos;wheres&apos;][] = [
            &apos;column&apos; =&amp;gt; $column,
            &apos;operator&apos; =&amp;gt; &apos;IN&apos;,
            &apos;value&apos; =&amp;gt; $values,
            &apos;boolean&apos; =&amp;gt; $boolean
        ];
        return $this;
    }

    /**
     * WHERE NOT IN条件
     */
    public function whereNotIn(string $column, array $values, string $boolean = &apos;AND&apos;): self
    {
        $this-&amp;gt;query[&apos;wheres&apos;][] = [
            &apos;column&apos; =&amp;gt; $column,
            &apos;operator&apos; =&amp;gt; &apos;NOT IN&apos;,
            &apos;value&apos; =&amp;gt; $values,
            &apos;boolean&apos; =&amp;gt; $boolean
        ];
        return $this;
    }

    /**
     * GROUP BY
     */
    public function groupBy(...$groups): self
    {
        $this-&amp;gt;query[&apos;groups&apos;] = array_merge($this-&amp;gt;query[&apos;groups&apos;], $groups);
        return $this;
    }

    /**
     * HAVING条件
     */
    public function having($column, $operator, $value = null): self
    {
        if (func_num_args() === 2) {
            $value = $operator;
            $operator = &apos;=&apos;;
        }

        $this-&amp;gt;query[&apos;havings&apos;][] = compact(&apos;column&apos;, &apos;operator&apos;, &apos;value&apos;);

        return $this;
    }

    /**
     * ORDER BY
     */
    public function orderBy($column, $direction = &apos;ASC&apos;): self
    {
        $this-&amp;gt;query[&apos;orders&apos;][$column] = strtoupper($direction) === &apos;ASC&apos; ? &apos;ASC&apos; : &apos;DESC&apos;;
        return $this;
    }

    /**
     * LIMIT
     */
    public function limit(int $limit): self
    {
        $this-&amp;gt;query[&apos;limit&apos;] = $limit;
        return $this;
    }

    /**
     * OFFSET
     */
    public function offset(int $offset): self
    {
        $this-&amp;gt;query[&apos;offset&apos;] = $offset;
        return $this;
    }

    /**
     * 执行查询获取结果
     */
    public function get(): array
    {
        $sql = $this-&amp;gt;toSql();
        // 执行后自动重置查询条件
//        $this-&amp;gt;query = $this-&amp;gt;freshQuery();
        return $this-&amp;gt;clickHouseService-&amp;gt;select($sql);
    }

    /**
     * 执行原生 SQL 查询（返回结果集）
     * @param string $sql
     * @return array
     */
    public function query(string $sql): array
    {
        return $this-&amp;gt;clickHouseService-&amp;gt;select($sql);
    }

    /**
     * 获取第一条记录
     */
    public function first(): ?array
    {
        $results = $this-&amp;gt;limit(1)-&amp;gt;get();
        return $results[0] ?? null;
    }

    /**
     * 安全转义值
     * @param mixed $value
     * @return string
     */
    protected function quoteValue(mixed $value): string
    {
        if (is_null($value)) {
            return &apos;NULL&apos;;
        }
        if ($value == &quot;&apos;&apos;&quot;) {
            return &quot;&apos;&apos;&quot;;
        }
        if (is_bool($value)) {
            return $value ? &apos;1&apos; : &apos;0&apos;;
        }
        if (is_int($value) || is_float($value)) {
            return (string)$value;
        }
        // 如果是字段引用（包含点号），不要加引号
        if (is_string($value) &amp;amp;&amp;amp; str_contains($value, &apos;.&apos;)) {
            return $value;
        }
        // 字符串转义
        return &quot;&apos;&quot; . $value . &quot;&apos;&quot;;
    }

    /**
     * 构建SQL语句
     */
    public function toSql(): string
    {
        // 确保 FROM 子句包含别名
        if ($this-&amp;gt;query[&apos;alias&apos;] &amp;amp;&amp;amp; !str_contains($this-&amp;gt;query[&apos;from&apos;], &apos; AS &apos;)) {
            $this-&amp;gt;query[&apos;from&apos;] = &quot;{$this-&amp;gt;getTable()} AS {$this-&amp;gt;query[&apos;alias&apos;]}&quot;;
        }
        $from = $this-&amp;gt;query[&apos;from&apos;];
        $sql = &quot;SELECT {$this-&amp;gt;query[&apos;select&apos;]} FROM {$from}&quot;;

        // 处理所有JOIN类型
        foreach ($this-&amp;gt;query[&apos;joins&apos;] as $join) {
            $hint = $join[&apos;hint&apos;] ?? &apos;&apos;;
            $sql .= &quot; {$hint} {$join[&apos;type&apos;]} JOIN {$join[&apos;table&apos;]}&quot;;

            if (!empty($join[&apos;multi_on&apos;])) {
                // 多条件ON处理
                $onClauses = [];
                foreach ($join[&apos;on&apos;] as $on) {
                    $value = $on[&apos;second&apos;];
                    $operator = strtoupper($on[&apos;operator&apos;]);

                    // 处理IN条件
                    if (in_array($operator, [&apos;IN&apos;, &apos;NOT IN&apos;])) {
                        // 空数组处理
                        if (is_array($value) &amp;amp;&amp;amp; empty($value)) {
                            // IN () 改为 1=0 (永远为假)
                            // NOT IN () 改为 1=1 (永远为真)
                            $onClauses[] = $operator === &apos;IN&apos; ? &apos;1=0&apos; : &apos;1=1&apos;;
                            continue;
                        }

                        $quotedValues = array_map([$this, &apos;quoteValue&apos;], $value);
                        $valueString = implode(&apos;, &apos;, $quotedValues);
                        $onClauses[] = &quot;{$on[&apos;first&apos;]} {$operator} ({$valueString})&quot;;
                    } else {
                        $quotedValue = $this-&amp;gt;quoteValue($value);
                        $onClauses[] = &quot;{$on[&apos;first&apos;]} {$operator} {$quotedValue}&quot;;
                    }
                }
                $sql .= &quot; ON &quot; . implode(&apos; AND &apos;, $onClauses);
            } else {
                // 单条件ON处理
                $value = $join[&apos;on&apos;][&apos;second&apos;];
                $operator = strtoupper($join[&apos;on&apos;][&apos;operator&apos;]);

                if (in_array($operator, [&apos;IN&apos;, &apos;NOT IN&apos;])) {
                    // 空数组处理
                    if (is_array($value) &amp;amp;&amp;amp; empty($value)) {
                        // IN () 改为 1=0 (永远为假)
                        // NOT IN () 改为 1=1 (永远为真)
                        $condition = $operator === &apos;IN&apos; ? &apos;1=0&apos; : &apos;1=1&apos;;
                        $sql .= &quot; ON {$condition}&quot;;
                    } else {
                        $quotedValues = array_map([$this, &apos;quoteValue&apos;], $value);
                        $valueString = implode(&apos;, &apos;, $quotedValues);
                        $sql .= &quot; ON {$join[&apos;on&apos;][&apos;first&apos;]} {$operator} ({$valueString})&quot;;
                    }
                } else {
                    $quotedValue = $this-&amp;gt;quoteValue($value);
                    $sql .= &quot; ON {$join[&apos;on&apos;][&apos;first&apos;]} {$operator} {$quotedValue}&quot;;
                }
            }
        }

        // WHERE
        if ($this-&amp;gt;query[&apos;wheres&apos;]) {
            $whereClauses = [];
            $firstWhere = true;
            foreach ($this-&amp;gt;query[&apos;wheres&apos;] as $where) {
                $boolean = $firstWhere ? &apos;&apos; : ($where[&apos;boolean&apos;] ?? &apos;AND&apos;);
                $firstWhere = false;

                $value = $where[&apos;value&apos;];
                $operator = strtoupper($where[&apos;operator&apos;]);

                // 处理IN条件
                if (in_array($operator, [&apos;IN&apos;, &apos;NOT IN&apos;])) {
                    // 空数组处理
                    if (is_array($value) &amp;amp;&amp;amp; empty($value)) {
                        // IN () 改为 1=0 (永远为假)
                        // NOT IN () 改为 1=1 (永远为真)
                        $whereClauses[] = $boolean . &apos; &apos; . ($operator === &apos;IN&apos; ? &apos;1=0&apos; : &apos;1=1&apos;);
                        continue;
                    }

                    $quotedValues = array_map([$this, &apos;quoteValue&apos;], $value);
                    $valueString = implode(&apos;, &apos;, $quotedValues);
                    $whereClauses[] = &quot;{$boolean} {$where[&apos;column&apos;]} {$operator} ({$valueString})&quot;;
                } else {
                    $quotedValue = $this-&amp;gt;quoteValue($value);
                    $whereClauses[] = &quot;{$boolean} {$where[&apos;column&apos;]} {$operator} {$quotedValue}&quot;;
                }
            }
            $sql .= &apos; WHERE &apos; . ltrim(implode(&apos; &apos;, $whereClauses));
        }

        // GROUP BY
        if ($this-&amp;gt;query[&apos;groups&apos;]) {
            $sql .= &apos; GROUP BY &apos; . implode(&apos;, &apos;, $this-&amp;gt;query[&apos;groups&apos;]);
        }

        // HAVING
        if ($this-&amp;gt;query[&apos;havings&apos;]) {
            $havingClauses = [];
            foreach ($this-&amp;gt;query[&apos;havings&apos;] as $having) {
                $quotedValue = $this-&amp;gt;quoteValue($having[&apos;value&apos;]);
                $havingClauses[] = &quot;{$having[&apos;column&apos;]} {$having[&apos;operator&apos;]} {$quotedValue}&quot;;
            }
            $sql .= &apos; HAVING &apos; . implode(&apos; AND &apos;, $havingClauses);
        }

        // ORDER BY
        if ($this-&amp;gt;query[&apos;orders&apos;]) {
            $orderClauses = [];
            foreach ($this-&amp;gt;query[&apos;orders&apos;] as $column =&amp;gt; $direction) {
                $orderClauses[] = &quot;{$column} {$direction}&quot;;
            }
            $sql .= &apos; ORDER BY &apos; . implode(&apos;, &apos;, $orderClauses);
        }

        // LIMIT
        if ($this-&amp;gt;query[&apos;limit&apos;]) {
            $sql .= &apos; LIMIT &apos; . $this-&amp;gt;query[&apos;limit&apos;];
            if ($this-&amp;gt;query[&apos;offset&apos;]) {
                $sql .= &apos; OFFSET &apos; . $this-&amp;gt;query[&apos;offset&apos;];
            }
        }

        return $sql;
    }



    /**
     * 聚合方法
     */
    public function count(string $column = &apos;&apos;): int
    {
        $result = $this-&amp;gt;select(&quot;COUNT({$column}) as aggregate&quot;)-&amp;gt;first();
        return (int)($result[&apos;aggregate&apos;] ?? 0);
    }

    public function avg(string $column): float
    {
        $result = $this-&amp;gt;select(&quot;AVG({$column}) as aggregate&quot;)-&amp;gt;first();
        return (float)($result[&apos;aggregate&apos;] ?? 0);
    }

    public function sum(string $column): float
    {
        $result = $this-&amp;gt;select(&quot;SUM({$column}) as aggregate&quot;)-&amp;gt;first();
        return (float)($result[&apos;aggregate&apos;] ?? 0);
    }

    public function max(string $column)
    {
        $result = $this-&amp;gt;select(&quot;MAX({$column}) as aggregate&quot;)-&amp;gt;first();
        return $result[&apos;aggregate&apos;] ?? null;
    }

    public function min(string $column)
    {
        $result = $this-&amp;gt;select(&quot;MIN({$column}) as aggregate&quot;)-&amp;gt;first();
        return $result[&apos;aggregate&apos;] ?? null;
    }

    /**
     * 插入数据
     */
    public function insert(array $data): bool
    {
        if (empty($data)) return false;

        $columns = array_keys($data);
        return $this-&amp;gt;clickHouseService-&amp;gt;batchInsert(
            $this-&amp;gt;getTable(),
            $columns,
            [array_values($data)]
        );
    }

    /**
     * 批量插入
     */
    public function batchInsert(array $columns, array $rows): bool
    {
        return $this-&amp;gt;clickHouseService-&amp;gt;batchInsert(
            $this-&amp;gt;getTable(),
            $columns,
            $rows
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 创建查询类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Repository;

class AdminCkRepository extends AbstractClickHouseRepository
{
    protected string $table = &apos;admin&apos;;

    /**
     * 排除已删除的查询范围
     * @param string $alias
     * @return self
     */
    public function scopeNormal(string $alias = &apos;&apos;): self
    {
        $alias = $alias ? &quot;{$alias}.&quot;: &apos;&apos;;
        return $this-&amp;gt;where(&quot;{$alias}delete_time&quot;, &apos;IS&apos;, null);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 使用方式&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是 每次查询都需要调用newQuery()方法初始化，否则内存共享会出现意料之外的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;adminCkRepository
            -&amp;gt;newQuery()
            -&amp;gt;alias(&apos;ad&apos;)
            -&amp;gt;select([
                &apos;ad.id AS institution_partner_id&apos;,
                &apos;MAX(ad.avatar) AS institution_partner_avatar&apos;,
                &apos;MAX(ad.nickname) AS institution_partner_name&apos;,
                &apos;countIf(sl.status = &apos; . KsSendSamplesLogEnum::STATUS_REVIEW_PASS. &apos;) AS count_audited_success&apos;,
                &apos;countIf(sl.status = &apos; . KsSendSamplesLogEnum::STATUS_DELIVERED. &apos;) AS count_express_success&apos;,
            ])
            -&amp;gt;leftJoinMultiOn($sample_log_table, [
                [&apos;ad.id&apos;, &apos;=&apos;, &apos;sl.admin_user_id&apos;],
                [&apos;sl.create_time&apos;, &apos;&amp;gt;=&apos;, $this-&amp;gt;params-&amp;gt;fx_user_start_time],
                [&apos;sl.create_time&apos;, &apos;&amp;lt;=&apos;, $this-&amp;gt;params-&amp;gt;fx_user_end_time],
                [&apos;sl.source&apos;, &apos;=&apos;, KsSendSamplesLogEnum::SOURCE_ADMIN]
            ], &apos;sl&apos;)
            -&amp;gt;where(&apos;ad.role_id&apos;, $role_id)
            -&amp;gt;scopeNormal(&apos;ad&apos;)
            -&amp;gt;groupBy(&apos;institution_partner_id&apos;)
            -&amp;gt;orderBy(&apos;count_audited_success&apos;, &apos;DESC&apos;)
            -&amp;gt;orderBy(&apos;institution_partner_id&apos;, &apos;DESC&apos;)
					  -&amp;gt;limit($pageSize)-&amp;gt;offset($offset)-&amp;gt;get();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>⛳️mac使用brew</title><link>https://www.corgi37.top/posts/mac-shi-yong-brew/</link><guid isPermaLink="true">https://www.corgi37.top/posts/mac-shi-yong-brew/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;安装Homebrew/使用国内源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/zsh -c &quot;$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据提示安装即可&lt;/p&gt;
&lt;p&gt;验证是否安装成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew -v
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>⛳️mac使用netcat</title><link>https://www.corgi37.top/posts/mac-shi-yong-netcat/</link><guid isPermaLink="true">https://www.corgi37.top/posts/mac-shi-yong-netcat/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;安装&lt;code&gt;netcat&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install netcat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nc&lt;/code&gt;是一个功能强大的网络工具，它可以用于读写网络连接。&lt;/p&gt;
&lt;p&gt;使用方法：打开终端，输入&lt;code&gt;nc -zv &amp;lt;主机地址&amp;gt; &amp;lt;端口号&amp;gt;&lt;/code&gt;，然后回车。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nc -zv example.com 80
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;-z：表示扫描时不发送任何数据。
-v：表示详细模式，会显示更多的信息。
如果端口开放，你会看到类似Connection to example.com 80 port [tcp/http] succeeded!的消息。如果端口关闭或不可达，你会看到一个连接失败的错误信息。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>⛳️mac使用tree</title><link>https://www.corgi37.top/posts/mac-shi-yong-tree/</link><guid isPermaLink="true">https://www.corgi37.top/posts/mac-shi-yong-tree/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;安装tree&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install tree
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能会遇到网络问题 挂上代理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http_proxy=http://代理:xxxx https_proxy=http://代理:xxxx brew install tree
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>🧊Linux设置时区</title><link>https://www.corgi37.top/posts/linux-she-zhi-shi-qu/</link><guid isPermaLink="true">https://www.corgi37.top/posts/linux-she-zhi-shi-qu/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看当前时区:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timedatectl status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会显示当前的时区设置。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置时区为上海:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timedatectl set-timezone Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令会将系统时区设置为上海时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;验证时区是否已经更改:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timedatectl status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会显示更新后的时区信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果需要同步时间,可以使用以下命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timedatectl set-ntp true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令会启用 NTP 时间同步服务,确保系统时间保持准确。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再次检查时间和时区:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;date
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会显示当前的日期和时间,确认已经是上海时间。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>🍜Hyperf3.1使用websocket实现数据大屏</title><link>https://www.corgi37.top/posts/hyperf31-shi-yong-websocket-shi-xian-shu-ju-da-ping/</link><guid isPermaLink="true">https://www.corgi37.top/posts/hyperf31-shi-yong-websocket-shi-xian-shu-ju-da-ping/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;基于Hyperf+ WebSocket+redis实现数据大屏的实时推送&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 安装扩展&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;安装 websocket 服务端&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;composer require hyperf/websocket-server
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 配置server&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在config/autoload/server.php增加配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
use Hyperf\Server\Event;
use Hyperf\Server\Server;
use Swoole\Constant;

return [
    &apos;mode&apos; =&amp;gt; SWOOLE_PROCESS,
    &apos;servers&apos; =&amp;gt; [
        [
            &apos;name&apos; =&amp;gt; &apos;http&apos;,
            &apos;type&apos; =&amp;gt; Server::SERVER_HTTP,
            &apos;host&apos; =&amp;gt; &apos;0.0.0.0&apos;,
            &apos;port&apos; =&amp;gt; 9501,
            &apos;sock_type&apos; =&amp;gt; SWOOLE_SOCK_TCP,
            &apos;callbacks&apos; =&amp;gt; [
                Event::ON_REQUEST =&amp;gt; [Hyperf\HttpServer\Server::class, &apos;onRequest&apos;],
            ],
            &apos;options&apos; =&amp;gt; [
                // Whether to enable request lifecycle event
                &apos;enable_request_lifecycle&apos; =&amp;gt; false,
            ],
        ],
        // 增加部分
        [
            &apos;name&apos; =&amp;gt; &apos;wsDataScreen&apos;,
            &apos;type&apos; =&amp;gt; Server::SERVER_WEBSOCKET,
            &apos;host&apos; =&amp;gt; &apos;0.0.0.0&apos;,
            &apos;port&apos; =&amp;gt; 9503,
            &apos;sock_type&apos; =&amp;gt; SWOOLE_SOCK_TCP,
            &apos;callbacks&apos; =&amp;gt; [
                Event::ON_HAND_SHAKE =&amp;gt; [Hyperf\WebSocketServer\Server::class, &apos;onHandShake&apos;],
                Event::ON_MESSAGE =&amp;gt; [Hyperf\WebSocketServer\Server::class, &apos;onMessage&apos;],
                Event::ON_CLOSE =&amp;gt; [Hyperf\WebSocketServer\Server::class, &apos;onClose&apos;],
            ],
            &apos;settings&apos; =&amp;gt; [
                // 心跳检测配置
                &apos;heartbeat_idle_time&apos; =&amp;gt; 600,  // 连接最大空闲时间（秒）
                &apos;heartbeat_check_interval&apos; =&amp;gt; 60, // 心跳检测间隔（秒）
                &apos;open_websocket_ping_frame&apos; =&amp;gt; false, // 开启PING帧自动回复
//                &apos;open_websocket_pong_frame&apos; =&amp;gt; true, // 开启PONG帧处理
            ],
        ],
    ],
    &apos;settings&apos; =&amp;gt; [
        Constant::OPTION_ENABLE_COROUTINE =&amp;gt; true, // 开启内置协程
        Constant::OPTION_WORKER_NUM =&amp;gt; swoole_cpu_num(), // 设置启动的 Worker 进程数
        Constant::OPTION_PID_FILE =&amp;gt; BASE_PATH . &apos;/runtime/hyperf.pid&apos;, // master 进程的 PID
        Constant::OPTION_OPEN_TCP_NODELAY =&amp;gt; true, // TCP 连接发送数据时会关闭 Nagle 合并算法，立即发往客户端连接
        Constant::OPTION_MAX_COROUTINE =&amp;gt; 100000, // 设置当前工作进程最大协程数量
        Constant::OPTION_OPEN_HTTP2_PROTOCOL =&amp;gt; true, // 启用 HTTP2 协议解析
        Constant::OPTION_MAX_REQUEST =&amp;gt; 100000, // 设置 worker 进程的最大任务数
        Constant::OPTION_SOCKET_BUFFER_SIZE =&amp;gt; 2 * 1024 * 1024, // 配置客户端连接的缓存区长度
        Constant::OPTION_BUFFER_OUTPUT_SIZE =&amp;gt; 2 * 1024 * 1024,

        // 添加任务工作进程配置
        &apos;task_worker_num&apos; =&amp;gt; swoole_cpu_num(), // 根据服务器CPU核心数设置
        &apos;task_enable_coroutine&apos; =&amp;gt; true, // 启用协程支持
    ],
    &apos;callbacks&apos; =&amp;gt; [
        Event::ON_WORKER_START =&amp;gt; [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, &apos;onWorkerStart&apos;],
        Event::ON_PIPE_MESSAGE =&amp;gt; [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, &apos;onPipeMessage&apos;],
        Event::ON_WORKER_EXIT =&amp;gt; [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, &apos;onWorkerExit&apos;],

        Event::ON_TASK =&amp;gt; [Hyperf\Framework\Bootstrap\TaskCallback::class, &apos;onTask&apos;],
        Event::ON_FINISH =&amp;gt; [Hyperf\Framework\Bootstrap\FinishCallback::class, &apos;onFinish&apos;],
    ],
];

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 配置路由&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在config/routes.php下新增&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Router::addServer(&apos;wsDataScreen&apos;, function () {
    Router::get(&apos;/wsDataScreen/ws&apos;, \App\Module\DataScreen\Controller\WebSocketController::class);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 中间件配置&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;可以在config/autoload/middlewares.php配置中间件，但是只有握手阶段会走该中间件，所以我这里不配置，因为我需要发送消息阶段也需要鉴权&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&apos;wsDataScreen&apos; =&amp;gt; [
//        \App\Middleware\DataScreenAuthMiddleware::class
    ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 创建控制器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在Module/DataScreen/Controller/WebSocketController.php下创建控制器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
//declare(strict_types=1);

namespace App\Module\DataScreen\Controller;

use App\Common\Enum\AuthGuardEnum;
use App\Common\Trait\RedisKeyTrait;
use App\Middleware\DataScreenAuthMiddleware;
use App\Module\DataScreen\Business\WebSocketBusiness;
use App\Module\DataScreen\Enum\RedisKeyEnum;
use App\Module\DataScreen\Repository\AdminDataRuleRepository;
use App\Module\DataScreen\Request\WebSocketRequest;
use Hyperf\WebSocketServer\Context;
use Hyperf\Contract\OnCloseInterface;
use Hyperf\Contract\OnMessageInterface;
use Hyperf\Contract\OnOpenInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Engine\WebSocket\Frame;
use Hyperf\Engine\WebSocket\Response;
use Hyperf\Redis\Redis;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\WebSocketServer\Constant\Opcode;
use Qbhy\HyperfAuth\AuthManager;


class WebSocketController implements OnMessageInterface, OnOpenInterface, OnCloseInterface
{
    use RedisKeyTrait;

    #[Inject]
    protected AuthManager $auth;

    #[Inject]
    private Redis $redis;

    #[Inject]
    private ValidatorFactoryInterface $validationFactory;

    #[Inject]
    private WebSocketRequest $webSocketRequest;

    #[Inject]
    private AdminDataRuleRepository $adminDataRuleRepository;

    #[Inject]
    private WebSocketBusiness $webSocketBusiness;


    public function onMessage($server, $frame): void
    {
        $alive_key = $this-&amp;gt;generateKeyPrefix(RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD, version: 0);
        $response = (new Response($server))-&amp;gt;init($frame);
        // 客户端ID
        $fd = $frame-&amp;gt;fd;

        // 获取用户信息
        $user = Context::get(&quot;user:{$fd}&quot;);
        if (!$user) {
            $server-&amp;gt;disconnect($fd, 401, &apos;请先登录&apos;);
            return;
        }

        // 获取用户数据权限
        $data_rule = Context::get(&quot;user_data_rule:{$user-&amp;gt;id}&quot;);


        if($frame-&amp;gt;opcode == Opcode::TEXT &amp;amp;&amp;amp; $frame-&amp;gt;data == &apos;ping&apos;) {
            // 手动处理心跳帧
            $response-&amp;gt;push(new Frame(opcode: Opcode::TEXT, payloadData: &apos;pong&apos;));
            // 添加到存活列表
            $this-&amp;gt;redis-&amp;gt;sAdd($alive_key, $fd);
            // 续期
            $this-&amp;gt;redis-&amp;gt;expire($alive_key, RedisKeyEnum::ALIVE_TTL);
            return;
        }

        try {
            $data = json_decode($frame-&amp;gt;data, true, 512, JSON_THROW_ON_ERROR);

            // 使用验证器验证数据
            $validator = $this-&amp;gt;validationFactory-&amp;gt;make(
                $data,
                $this-&amp;gt;webSocketRequest-&amp;gt;rules($data),
                $this-&amp;gt;webSocketRequest-&amp;gt;messages()
            );

            if ($validator-&amp;gt;fails()) {
                $error = $validator-&amp;gt;errors()-&amp;gt;first();
                throw new \InvalidArgumentException($error);
            }

            // 统一参数
            $params = $this-&amp;gt;webSocketBusiness-&amp;gt;formatParams($data, $data_rule);

            // redis存储 客户端=&amp;gt;筛选条件
            $this-&amp;gt;webSocketBusiness-&amp;gt;storeClientFilters($fd, $params);

            // 立即返回当前数据
            $census_data = $this-&amp;gt;webSocketBusiness-&amp;gt;getCurrentData($params);

            $response-&amp;gt;push(new Frame(payloadData: responseSuccess($census_data)));

        } catch (\Throwable $e) {
            $response-&amp;gt;push(new Frame(payloadData: responseError($e-&amp;gt;getMessage())));
        }
    }

    public function onClose($server, int $fd, int $reactorId): void
    {
        // 移除存活列表集合中指定的fd
        $alive_key = $this-&amp;gt;generateKeyPrefix(RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD, version: 0);
        $this-&amp;gt;redis-&amp;gt;sRem($alive_key, $fd);
        // 移除fd的筛选
        $filter_key = $this-&amp;gt;generateKeyPrefix(RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD_FILTER);
        $this-&amp;gt;redis-&amp;gt;hDel($filter_key, $fd);
    }

    public function onOpen($server, $request): void
    {
        $response = (new Response($server))-&amp;gt;init($request);
        try {

            if (!$this-&amp;gt;auth-&amp;gt;guard(AuthGuardEnum::DATA_SCREEN_JWT)-&amp;gt;check()) {
                throw new \RuntimeException(&apos;请先登录&apos;, 401);
            }

            // 将用户信息存储在上下文中，以便后续使用
            $user = $this-&amp;gt;auth-&amp;gt;guard(AuthGuardEnum::DATA_SCREEN_JWT)-&amp;gt;user();
            if (empty($user)) {
                throw new \RuntimeException(&apos;请先登录&apos;, 401);
            }

            Context::set(&quot;user:{$request-&amp;gt;fd}&quot;, $user);

            // 查询用户的数据权限
            $data_rule = $this-&amp;gt;adminDataRuleRepository-&amp;gt;getAdminDataRuleByAdminId($user-&amp;gt;id);
            Context::set(&quot;user_data_rule:{$user-&amp;gt;id}&quot;, $data_rule);

            $alive_key = $this-&amp;gt;generateKeyPrefix(RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD, version: 0);

            // 添加到存活列表
            $this-&amp;gt;redis-&amp;gt;sAdd($alive_key, $request-&amp;gt;fd);
            // 续期
            $this-&amp;gt;redis-&amp;gt;expire($alive_key, RedisKeyEnum::ALIVE_TTL);

            $response-&amp;gt;push(new Frame(payloadData: responseSuccess(&apos;连接成功&apos;)));
        } catch (\Throwable $e) {
            if ($e-&amp;gt;getCode() == 401) {
                $server-&amp;gt;disconnect($request-&amp;gt;fd, 401, $e-&amp;gt;getMessage());
                return;
            }
            $response-&amp;gt;push(new Frame(payloadData: responseError($e-&amp;gt;getMessage())));
        }

    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中使用到了RedisKeyTrait特征，主要是为了生成用户筛选唯一的ke y&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Common\Trait;

use App\Module\DataScreen\Enum\RedisKeyEnum;
use function Hyperf\Config\config;

trait RedisKeyTrait
{
    /**
     * 生成前缀
     * @param string $key
     * @param string $module
     * @param string $other_key
     * @return string
     */
    public function generateKeyPrefix(string $key, string $module = RedisKeyEnum::MODULE_DATA_SCREEN, string $other_key = &apos;&apos;, int $version = 1): string
    {
        $env = config(&apos;app_env&apos;);
        $version_str = !empty($version) ? &quot;_v{$version}&quot; : &apos;&apos;;
        return &quot;{$module}_{$env}_{$key}{$other_key}{$version_str}&quot;;
    }

    /**
     * 生成根据筛选条件定义的key
     * @param array $params
     * @return string
     */
    public function generateFilterGroupKey(array $params): string
    {
        return implode(&apos;_&apos;, array_merge(...array_map(fn($k, $v) =&amp;gt; [$k, $v], array_keys($params), $params)));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;使用到常量&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Enum;

class RedisKeyEnum
{
    // 存活时间
    const ALIVE_TTL = 7200;
    // 应用名称-数据大屏
    const MODULE_DATA_SCREEN = &apos;DataScreen&apos;;

    const MODULE_DATA_CENTER = &apos;DataCenter&apos;;

    // 存活的客户端key
    const REDIS_KEY_WEBSOCKET_FD = &apos;websocket_active_fd&apos;;
    // 客户端的筛选
    const REDIS_KEY_WEBSOCKET_FD_FILTER = &apos;websocket_fd_filter&apos;;
    // 筛选分组下的数据
    const REDIS_KEY_WEBSOCKET_FD_FILTER_GROUP = &apos;websocket_fd_filter_group&apos;;

    // 平台商品关联货品缓存
    const REDIS_KEY_WDT_GOODS_LINK = &apos;wdt_goods_link&apos;;

    // 交易漏斗缓存
    const REDIS_KEY_WDT_ORDER_TRANSFORM = &apos;wdt_order_transform&apos;;
    // 核心交易
    const REDIS_KEY_WDT_ORDER_CORE_API = &apos;wdt_order_core_api&apos;;
    // 订单结构
    const REDIS_KEY_WDT_ORDER_STRUCT = &apos;wdt_order_struct&apos;;
    // 售后
    const REDIS_KEY_WDT_ORDER_AFTER_SALE = &apos;wdt_order_after_sale&apos;;
    // 退款top
    const REDIS_KEY_WDT_ORDER_REFUND_TOP = &apos;wdt_order_refund_top&apos;;
    // 库存
    const REDIS_KEY_WDT_CENSUS_STOCK = &apos;wdt_census_stock&apos;;

    // 各地库存
    const REDIS_KEY_WDT_WAREHOUSE_STOCK = &apos;wdt_warehouse_stock&apos;;

    // 地图
    const REDIS_KEY_WDT_OVERVIEW_MAP = &apos;wdt_overview_map&apos;;

    // 退款占比
    const REDIS_KEY_WDT_REFUND_RATE = &apos;wdt_refund_rate&apos;;

    // 入账金额折线图
    const REDIS_KEY_WDT_PAY_MAP  = &apos;wdt_pay_map&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;验证器验证规则&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Request;

use Carbon\Carbon;

class WebSocketRequest
{
    /**
     * 验证规则
     */
    public function rules(array $requestData): array
    {
        return [
            &apos;module&apos; =&amp;gt; &apos;required|in:1,2,3,4,5,6,7,8&apos;,
            &apos;platform&apos; =&amp;gt; &apos;required|in:0,1,2&apos;,
            &apos;date_type&apos;  =&amp;gt; &apos;required|in:0,1&apos;,
            &apos;date_value&apos; =&amp;gt; &apos;required_if:date_type,0|in:0,1,2,3,4&apos;,
            &apos;date_start&apos; =&amp;gt; [
                &apos;required_if:date_type,1&apos;,
                &apos;date&apos;,
                &apos;date_format:Y-m-d&apos;,
                &apos;before_or_equal:date_end&apos;,
                function ($attr, $value, $fail) use ($requestData) {
                    if (($requestData[&apos;date_type&apos;] ?? 0) == 1 &amp;amp;&amp;amp; !empty($requestData[&apos;date_end&apos;] ?? &apos;&apos;)) {
                        $start = Carbon::parse($value);
                        $end = Carbon::parse($requestData[&apos;date_end&apos;]);

                        if ($start-&amp;gt;diffInDays($end) &amp;gt; 31) {
                            $fail(&apos;日期范围不能超过31天&apos;);
                        }
                    }
                }
            ],
            &apos;date_end&apos; =&amp;gt; &apos;required_if:date_type,1|date|date_format:Y-m-d|after_or_equal:date_start&apos;,
            &apos;module_user_id&apos; =&amp;gt; [
                &apos;required_if:module,5,6,7&apos;,
                &apos;not_in:undefined&apos;,
            ],
            &apos;position&apos; =&amp;gt; &apos;required|in:0,1,2&apos;,
            &apos;fx_user_date_start&apos; =&amp;gt; [
                &apos;required_if:position,1,2&apos;,
                &apos;date&apos;,
                &apos;date_format:Y-m-d&apos;,
                &apos;before_or_equal:fx_user_date_end&apos;,
                function ($attr, $value, $fail) use ($requestData) {
                    if (!empty($requestData[&apos;fx_user_date_end&apos;] ?? &apos;&apos;)) {
                        $start = Carbon::parse($value);
                        $end = Carbon::parse($requestData[&apos;fx_user_date_end&apos;]);

                        if ($start-&amp;gt;diffInDays($end) &amp;gt; 31) {
                            $fail(&apos;日期范围不能超过31天&apos;);
                        }
                    }
                }
            ],
            &apos;fx_user_date_end&apos; =&amp;gt; &apos;required_if:position,1,2|date|date_format:Y-m-d|after_or_equal:fx_user_date_start&apos;,
            &apos;fx_user_page&apos; =&amp;gt; &apos;required_if:position,1,2|numeric|min:1&apos;,
        ];
    }

    /**
     * 自定义错误消息
     */
    public function messages(): array
    {
        return [
            &apos;module.required&apos; =&amp;gt; &apos;模块不能为空&apos;,
            &apos;module.in&apos; =&amp;gt; &apos;模块参数非法&apos;,

            &apos;module_user_id.required_if&apos; =&amp;gt; &apos;相关用户ID不能为空&apos;,
            &apos;module_user_id.not_in&apos; =&amp;gt; &apos;相关用户ID不能为空！&apos;,

            &apos;platform.required&apos; =&amp;gt; &apos;平台不能为空&apos;,
            &apos;platform.in&apos; =&amp;gt; &apos;平台参数非法&apos;,

            &apos;date_type.required&apos; =&amp;gt; &apos;时间类型不能为空&apos;,
            &apos;date_type.in&apos; =&amp;gt; &apos;时间类型参数非法&apos;,

            &apos;date_value.required_if&apos; =&amp;gt; &apos;时间筛选不能为空&apos;,
            &apos;date_value.in&apos; =&amp;gt; &apos;时间筛选参数非法&apos;,

            &apos;date_start.required_if&apos; =&amp;gt; &apos;开始时间不能为空&apos;,
            &apos;date_start.date&apos; =&amp;gt; &apos;开始时间参数非法&apos;,
            &apos;date_start.date_format&apos; =&amp;gt; &apos;开始时间格式必须为YYYY-MM-DD&apos;,
            &apos;date_start.before_or_equal&apos; =&amp;gt; &apos;开始时间参数必须小于等于结束时间&apos;,

            &apos;date_end.required_if&apos; =&amp;gt; &apos;结束时间不能为空&apos;,
            &apos;date_end.date&apos; =&amp;gt; &apos;结束时间参数非法&apos;,
            &apos;date_end.date_format&apos; =&amp;gt; &apos;结束时间格式必须为YYYY-MM-DD&apos;,
            &apos;date_end.before_or_equal&apos; =&amp;gt; &apos;结束时间参数必须小于等于开始时间&apos;,

            &apos;position.required&apos; =&amp;gt; &apos;当前停留的位置参数不能为空&apos;,

            &apos;fx_user_date_start.required_if&apos; =&amp;gt; &apos;开始时间不能为空&apos;,
            &apos;fx_user_date_start.date&apos; =&amp;gt; &apos;开始时间参数非法&apos;,
            &apos;fx_user_date_start.date_format&apos; =&amp;gt; &apos;开始时间格式必须为YYYY-MM-DD&apos;,
            &apos;fx_user_date_start.before_or_equal&apos; =&amp;gt; &apos;开始时间参数必须小于等于结束时间&apos;,

            &apos;fx_user_date_end.required_if&apos; =&amp;gt; &apos;结束时间不能为空&apos;,
            &apos;fx_user_date_end.date&apos; =&amp;gt; &apos;结束时间参数非法&apos;,
            &apos;fx_user_date_end.date_format&apos; =&amp;gt; &apos;结束时间格式必须为YYYY-MM-DD&apos;,
            &apos;fx_user_date_end.before_or_equal&apos; =&amp;gt; &apos;结束时间参数必须小于等于开始时间&apos;,

            &apos;fx_user_page.required_if&apos; =&amp;gt; &apos;页码不能为空&apos;,
            &apos;fx_user_page.numeric&apos; =&amp;gt; &apos;页码必须为数字&apos;,
            &apos;fx_user_page.min&apos; =&amp;gt; &apos;页码最小值为1&apos;,
        ];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中使用到了查询用户拥有的数据权限（无需关注）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare(strict_types=1);
namespace App\Module\DataScreen\Repository;

use App\Common\Utils\BaseRepository;
use App\Module\DataScreen\Model\Admin;
use App\Module\DataScreen\Model\AdminDataRule;
use App\Module\DataScreen\Model\DataRule;

class AdminDataRuleRepository extends BaseRepository
{
    public function __construct()
    {
        $this-&amp;gt;setModel(new AdminDataRule());
    }

    public function getByAdminId($adminId)
    {
        return $this-&amp;gt;getModel()::query()-&amp;gt;where(&apos;admin_id&apos;, $adminId)-&amp;gt;get();
    }


    /**
     * 获取用户的数据权限
     * @param int $adminId
     * @return array
     */
    public function getAdminDataRuleByAdminId(int $adminId): array
    {
        $table_name = $this-&amp;gt;getModel()-&amp;gt;getTable();
        $data_rule_table_name = (new DataRule())-&amp;gt;getTable();
        return $this-&amp;gt;getModel()::query()
            -&amp;gt;select([
                &quot;{$table_name}.admin_id&quot;,
                &quot;{$data_rule_table_name}.type&quot;,
                &quot;{$data_rule_table_name}.step_type&quot;,
                &quot;{$data_rule_table_name}.rule_id&quot;,
                &quot;{$data_rule_table_name}.source_id&quot;,
            ])
            -&amp;gt;where(&quot;{$table_name}.admin_id&quot;, $adminId)
            -&amp;gt;join($data_rule_table_name, &quot;{$table_name}.rule_id&quot;, &apos;=&apos;, &quot;{$data_rule_table_name}.id&quot;)
            -&amp;gt;get()-&amp;gt;toArray();
    }


    /**
     * 删除旧的数据权限
     * @param int $adminId
     * @return int|mixed
     */
    public function deleteOldDataRules(int $adminId): mixed
    {
        return $this-&amp;gt;getModel()::query()-&amp;gt;where(&apos;admin_id&apos;, $adminId)-&amp;gt;delete();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 创建服务层&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;创建Module/DataScreen/Business/WebSocketBusiness.php&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Business;

use App\Common\Trait\RedisKeyTrait;
use App\Module\DataScreen\Common\Trait\DataCenterTrait;
use App\Module\DataScreen\Common\Trait\DataRuleTrait;
use App\Module\DataScreen\Enum\RedisKeyEnum;
use App\Module\DataScreen\Enum\WebSocketEnum;
use Carbon\Carbon;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;

class WebSocketBusiness
{
    use RedisKeyTrait;
    use DataCenterTrait;
    use DataRuleTrait;

    #[Inject]
    private Redis $redis;

    /**
     * 统一参数格式
     * @param array $data 传递参数
     * @param array $data_rule 数据权限
     * @return array
     */
    public function formatParams(array $data, array $data_rule): array
    {
        $params = [
            // 模块 1兵工场 2机构 3分销者 4产品 5单兵工场页面
            &apos;module&apos; =&amp;gt; intval($data[&apos;module&apos;] ?? WebSocketEnum::MODULE_BING_GONG_CHANG),
            // 模块相关用户ID
            &apos;module_user_id&apos; =&amp;gt; trim($data[&apos;module_user_id&apos;] ?? &apos;&apos;),
            // 平台 1快手 2视频号 3抖音
            &apos;platform&apos; =&amp;gt; intval($data[&apos;platform&apos;] ?? WebSocketEnum::PLATFORM_KS),
            // 时间类型：0时间选择 1自定义时间
            &apos;date_type&apos; =&amp;gt; intval($data[&apos;date_type&apos;] ?? WebSocketEnum::DATE_TYPE_SELECT),
            // 时间选择 1今日 2昨日 3本月 4上月（当时间类型为0时必传 为1的时候传0）
            &apos;date_value&apos; =&amp;gt; intval($data[&apos;date_value&apos;] ?? 0),
            // 开始时间（当时间类型为1时必传 为0的时候传&apos;&apos;）
            &apos;date_start&apos; =&amp;gt; trim($data[&apos;date_start&apos;] ?? &apos;&apos;),
            // 结束时间（当时间类型为1时必传 为0的时候传&apos;&apos;）
            &apos;date_end&apos; =&amp;gt; trim($data[&apos;date_end&apos;] ?? &apos;&apos;),

            // 当前页面：0首页 1分销者数据详情
            &apos;position&apos; =&amp;gt; intval($data[&apos;position&apos;] ?? WebSocketEnum::POSITION_INDEX),
            // 分销者数据详情时间筛选-开始时间
            &apos;fx_user_date_start&apos; =&amp;gt; trim($data[&apos;fx_user_date_start&apos;] ?? &apos;&apos;),
            // 分销者数据详情时间筛选-结束时间
            &apos;fx_user_date_end&apos; =&amp;gt; trim($data[&apos;fx_user_date_end&apos;] ?? &apos;&apos;),
            // 分销者用户编号
            &apos;fx_user_no&apos; =&amp;gt; trim($data[&apos;fx_user_no&apos;] ?? &apos;&apos;),
            // 分销者数据详情页码
            &apos;fx_user_page&apos; =&amp;gt; intval($data[&apos;fx_user_page&apos;] ?? 1),
        ];

        // 时间筛选
        [$params[&apos;start_time&apos;], $params[&apos;end_time&apos;]] = getTimeByType($params[&apos;date_type&apos;], $params[&apos;date_value&apos;], $params[&apos;date_start&apos;], $params[&apos;date_end&apos;]);
        // 分销者详情数据时间
        [$params[&apos;fx_user_start_time&apos;], $params[&apos;fx_user_end_time&apos;]] = getTimeByType(
            WebSocketEnum::DATE_TYPE_DIY,
            0,
            $params[&apos;fx_user_date_start&apos;],
            $params[&apos;fx_user_date_end&apos;]
        );

        $params = array_merge($params, $this-&amp;gt;getDataRuleParams($params, $data_rule));

        ksort($params);
        return $params;
    }

    public function storeClientFilters(int $fd, array $params): void
    {
        $json_params = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        $filter_key = $this-&amp;gt;generateKeyPrefix(RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD_FILTER);
        // 存储客户端筛选条件
        $this-&amp;gt;redis-&amp;gt;hSet($filter_key, strval($fd), $json_params);
    }


    /**
     * 获取当前数据
     * @param array $params
     * @return array
     * @throws \RedisException
     */
    public function getCurrentData(array $params): array
    {
        // 查询是否有缓存
        $other_key = $this-&amp;gt;generateFilterGroupKey($params);
        $filter_group_key = $this-&amp;gt;generateKeyPrefix(
            RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD_FILTER_GROUP,
            RedisKeyEnum::MODULE_DATA_SCREEN,
            &quot;:{$other_key}&quot;);
        $cache_data = $this-&amp;gt;redis-&amp;gt;get($filter_group_key) ?: &apos;&apos;;
        $data_raw = !empty($cache_data) ? json_decode($cache_data, true) : [];
        $data = ($data_raw[&apos;data&apos;] ?? []) ?: [];
        // 数据版本号
        $last_version = ($data_raw[&apos;version&apos;] ?? 0) ?: 0;
        if (!empty($data)) {
            return $data;
        }

        // 实时查询
        $data = $this-&amp;gt;dataCenter($params);
        // 设置缓存数据
        $this-&amp;gt;redis-&amp;gt;set($filter_group_key, json_encode([&apos;filter&apos; =&amp;gt; $params, &apos;data&apos; =&amp;gt; $data, &apos;version&apos; =&amp;gt; $last_version + 1, &apos;update_time&apos; =&amp;gt; time()]));
        $this-&amp;gt;redis-&amp;gt;expire($filter_group_key, 7200);

        return $data;
    }



}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中使用到了数据查询模块常量&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Enum;

class WebSocketEnum
{
    /** 模块 */
    const MODULE_BING_GONG_CHANG = 1;
    const MODULE_INSTITUTION = 2;
    const MODULE_FX_USER = 3;
    const MODULE_FX_PRODUCT = 4;
    const MODULE_SINGLE_BING_GONG_CHANG = 5;
    const MODULE_SINGLE_INSTITUTION = 6;
    const MODULE_SINGLE_FACTORY_FX_USER = 7;
    // 旺店通
    const MODULE_WANG_DAN_TONG = 8;

    const MODULE_DESC = [
        self::MODULE_BING_GONG_CHANG =&amp;gt; &apos;兵工场&apos;,
        self::MODULE_INSTITUTION =&amp;gt; &apos;机构&apos;,
        self::MODULE_FX_USER =&amp;gt; &apos;分销者&apos;,
        self::MODULE_FX_PRODUCT =&amp;gt; &apos;产品&apos;,
        self::MODULE_SINGLE_BING_GONG_CHANG =&amp;gt; &apos;单兵工场&apos;,
        self::MODULE_SINGLE_INSTITUTION =&amp;gt; &apos;单机构&apos;,
        self::MODULE_SINGLE_FACTORY_FX_USER =&amp;gt; &apos;分销者实时数据汇总&apos;,
        self::MODULE_WANG_DAN_TONG =&amp;gt; &apos;旺店通&apos;,
    ];

    /** 平台 */
    const PLATFORM_KS = 1;
    const PLATFORM_SPH = 2;
    const PLATFORM_DY = 3;
    const PLATFORM_WDT = 0;

    const PLATFORM_DESC = [
        self::PLATFORM_KS =&amp;gt; &apos;快手&apos;,
        self::PLATFORM_SPH =&amp;gt; &apos;视频号&apos;,
        self::PLATFORM_DY =&amp;gt; &apos;抖音&apos;,
        self::PLATFORM_WDT =&amp;gt; &apos;旺店通&apos;,
    ];


    /** 时间类型 */
    const DATE_TYPE_SELECT = 0;
    const DATE_TYPE_DIY = 1;

    const DATE_TYPE_DESC = [
        self::DATE_TYPE_SELECT =&amp;gt; &apos;时间选择&apos;,
        self::DATE_TYPE_DIY =&amp;gt; &apos;自定义时间&apos;,
    ];

    /** 时间选择 */
    const DATE_SELECT_TODAY = 1;
    const DATE_SELECT_YESTERDAY = 2;
    const DATE_SELECT_THIS_MONTH = 3;
    const DATE_SELECT_LAST_MONTH = 4;
    // 时间选择描述
    const DATE_SELECT_DESC = [
        self::DATE_SELECT_TODAY =&amp;gt; &apos;今日&apos;,
        self::DATE_SELECT_YESTERDAY =&amp;gt; &apos;昨日&apos;,
        self::DATE_SELECT_THIS_MONTH =&amp;gt; &apos;本月&apos;,
        self::DATE_SELECT_LAST_MONTH =&amp;gt; &apos;上月&apos;,
    ];

    // 上期的描述
    const DATE_PREVIOUS_DESC = [
        self::DATE_SELECT_TODAY =&amp;gt; &apos;昨日&apos;,
        self::DATE_SELECT_YESTERDAY =&amp;gt; &apos;前日&apos;,
        self::DATE_SELECT_THIS_MONTH =&amp;gt; &apos;上月&apos;,
        self::DATE_SELECT_LAST_MONTH =&amp;gt; &apos;前月&apos;,
    ];

    // 循环类型
    const FOR_TYPE_HOUR = 0;
    const FOR_TYPE_DAY = 1;

    const FOR_TYPE_DESC = [
        self::FOR_TYPE_HOUR =&amp;gt; &apos;小时&apos;,
        self::FOR_TYPE_DAY =&amp;gt; &apos;天&apos;,
    ];


    /** 当前停留的块 */
    const POSITION_INDEX = 0;
    const POSITION_FX_USER_DETAIL = 1;
    const POSITION_INSTITUTION_PARTNER_DETAIL = 2;

    const POSITION_DESC = [
        self::POSITION_INDEX =&amp;gt; &apos;首页&apos;,
        self::POSITION_FX_USER_DETAIL =&amp;gt; &apos;分销者数据详情&apos;,
        self::POSITION_INSTITUTION_PARTNER_DETAIL =&amp;gt; &apos;机构招商数据详情&apos;,
    ];
    // 默认页码
    const DEFAULT_PAGE = 10;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中使用到了助手函数 主要是获取开始时间、结束时间、以及折线图需要的数据&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;if (!function_exists(&apos;getTimeByType&apos;)) {
    /**
     * 根据时间类型获取时间
     */
    function getTimeByType(int $date_type, int $date_value = 0, string $date_start = &apos;&apos;, string $date_end = &apos;&apos;): array
    {
        $start_time = $end_time = $for_i = $for_type = 0;
        if ($date_type == WebSocketEnum::DATE_TYPE_SELECT) {
            switch ($date_value) {
                case WebSocketEnum::DATE_SELECT_TODAY:
                    $start_time = Carbon::today()-&amp;gt;startOfDay()-&amp;gt;timestamp;
                    $end_time = Carbon::today()-&amp;gt;endOfDay()-&amp;gt;timestamp;
                    $for_type = WebSocketEnum::FOR_TYPE_HOUR;
                    $for_i = 23;
                    break;
                case WebSocketEnum::DATE_SELECT_YESTERDAY:
                    $start_time = Carbon::yesterday()-&amp;gt;startOfDay()-&amp;gt;timestamp;
                    $end_time = Carbon::yesterday()-&amp;gt;endOfDay()-&amp;gt;timestamp;
                    $for_type = WebSocketEnum::FOR_TYPE_HOUR;
                    $for_i = 23;
                    break;
                case WebSocketEnum::DATE_SELECT_THIS_MONTH:
                    $start_time_handler = Carbon::now()-&amp;gt;startOfMonth();
                    $end_time_handler = Carbon::now()-&amp;gt;endOfMonth();
                    $for_i = $end_time_handler-&amp;gt;diffInDays($start_time_handler);
                    $start_time = $start_time_handler-&amp;gt;timestamp;
                    $end_time = $end_time_handler-&amp;gt;timestamp;
                    $for_type = WebSocketEnum::FOR_TYPE_DAY;
                    break;
                case WebSocketEnum::DATE_SELECT_LAST_MONTH:
                    $start_time_handler = Carbon::now()-&amp;gt;subMonth()-&amp;gt;startOfMonth();
                    $end_time_handler = Carbon::now()-&amp;gt;subMonth()-&amp;gt;endOfMonth();
                    $for_i = $end_time_handler-&amp;gt;diffInDays($start_time_handler);
                    $start_time = $start_time_handler-&amp;gt;timestamp;
                    $end_time = $end_time_handler-&amp;gt;timestamp;
                    $for_type = WebSocketEnum::FOR_TYPE_DAY;
                    break;
            }
        } elseif ($date_type == WebSocketEnum::DATE_TYPE_DIY) {
            if (empty($date_start) &amp;amp;&amp;amp; empty($date_end)) {
                return [$start_time, $end_time, $for_type, $for_i];
            }
            $start_time_handler = Carbon::parse($date_start)-&amp;gt;startOfDay();
            $end_time_handler = Carbon::parse($date_end)-&amp;gt;endOfDay();
            if ($start_time_handler-&amp;gt;isSameDay($end_time_handler)) {
                $for_type = WebSocketEnum::FOR_TYPE_HOUR;
                $for_i = 23;
            } else {
                $for_type = WebSocketEnum::FOR_TYPE_DAY;
                $for_i = $end_time_handler-&amp;gt;diffInDays($start_time_handler);
            }
            $start_time = $start_time_handler-&amp;gt;timestamp;
            $end_time = $end_time_handler-&amp;gt;timestamp;
        }

        return [$start_time, $end_time, $for_type, $for_i];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;还使用到了DataRuleTrait特征，主要是获取当前用户可查看的数据范围ID列表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Common\Trait;

use App\Module\DataScreen\Enum\DataRuleEnum;
use App\Module\DataScreen\Enum\WebSocketEnum;

trait DataRuleTrait
{

    /**
     * 数据权限
     * @param array $params
     * @param array $data_rule
     * @return string[]
     */
    public function getDataRuleParams(array $params, array $data_rule): array
    {
        $return_data = [
            &apos;rule_source_ids&apos; =&amp;gt; &apos;&apos;
        ];

        $data_rule_data = [];
        foreach ($data_rule as $rule) {
            $data_rule_data[$rule[&apos;type&apos;]][$rule[&apos;step_type&apos;]][] = $rule[&apos;source_id&apos;];
        }

        $module = intval($params[&apos;module&apos;]);
        if (!in_array($module, [WebSocketEnum::MODULE_BING_GONG_CHANG, WebSocketEnum::MODULE_INSTITUTION])) {
            return $return_data;
        }

        $return_data[&apos;rule_source_ids&apos;] = &apos;-1&apos;;
        if ($module == WebSocketEnum::MODULE_BING_GONG_CHANG) {
            $rule_source_ids = ($data_rule_data[DataRuleEnum::TYPE_FACTORY][DataRuleEnum::STEP_TYPE_FACTORY] ?? []) ?: [];
            $return_data[&apos;rule_source_ids&apos;] = !empty($rule_source_ids) ? implode(&apos;,&apos;, $rule_source_ids) : &apos;-1&apos;;
            return $return_data;
        }

        if ($module == WebSocketEnum::MODULE_INSTITUTION) {
            $step_type = DataRuleEnum::PLATFORM_STEP_TYPE_MAP[intval($params[&apos;platform&apos;])];
            $rule_source_ids = ($data_rule_data[DataRuleEnum::TYPE_INSTITUTION][$step_type] ?? []) ?: [];
            $return_data[&apos;rule_source_ids&apos;] = !empty($rule_source_ids) ? implode(&apos;,&apos;, $rule_source_ids) : &apos;-1&apos;;
            return $return_data;
        }

        return $return_data;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. 创建数据查询特征&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;创建数据查询中心特征Module/DataScreen/Common/Trait/DataCenterTrait.php，无需关注业务逻辑 只需要关注中心方法dataCenter()即可，每个Business需要有public WebSocketParam $params;属性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Common\Trait;

use App\Module\DataScreen\Business\OrderCensusBusiness;
use App\Module\DataScreen\Business\OrderChartBusiness;
use App\Module\DataScreen\Business\OrderRankBusiness;
use App\Module\DataScreen\Business\ProductBusiness;
use App\Module\DataScreen\Business\SendSampleBusiness;
use App\Module\DataScreen\Business\WdtOriginOrderBusiness;
use App\Module\DataScreen\Common\Params\WebSocketParam;
use App\Module\DataScreen\Enum\WebSocketEnum;
use Hyperf\Di\Annotation\Inject;

trait DataCenterTrait
{

    #[Inject]
    private WebSocketParam $webSocketParam;

    #[Inject]
    private OrderCensusBusiness $orderCensusBusiness;

    #[Inject]
    private SendSampleBusiness $sendSampleBusiness;


    #[Inject]
    private OrderRankBusiness $orderRankBusiness;

    #[Inject]
    private OrderChartBusiness $orderChartBusiness;

    #[Inject]
    private ProductBusiness $productBusiness;

    /**
     * 数据统一处理
     * @param array $params
     * @return array
     */
    public function dataCenter(array $params): array
    {

        $return_data = [];
        switch ($params[&apos;module&apos;]) {
            case WebSocketEnum::MODULE_BING_GONG_CHANG:
                // 兵工场模块
                $return_data = $this-&amp;gt;getBingGongChangModule($params);
                break;
            case WebSocketEnum::MODULE_INSTITUTION:
                // 机构模块
                $return_data = $this-&amp;gt;getInstitutionModule($params);
                break;
            case WebSocketEnum::MODULE_FX_USER:
                // 分销者模块
                $return_data = $this-&amp;gt;getFxUserModule($params);
                break;
            case WebSocketEnum::MODULE_FX_PRODUCT:
                // 产品模块
                $return_data = $this-&amp;gt;getProductModule($params);
                break;
            case WebSocketEnum::MODULE_SINGLE_BING_GONG_CHANG:
                // 单兵工场模块
                $return_data = $this-&amp;gt;getSingleBingGongChangModule($params);
                break;
            case WebSocketEnum::MODULE_SINGLE_INSTITUTION:
                // 单机构模块
                $return_data = $this-&amp;gt;getSingleInstitutionModule($params);
                break;
            case WebSocketEnum::MODULE_SINGLE_FACTORY_FX_USER:
                // 分销者实时数据汇总模块
                $return_data = $this-&amp;gt;getSingleFactoryFxUserModule($params);
                break;
            case WebSocketEnum::MODULE_WANG_DAN_TONG:
                // 产品模块
                $return_data = $this-&amp;gt;getWdtModule($params);
                break;
        }
        return $return_data;
    }


    /**
     * 获取兵工场模块数据
     * @param array $params
     * @return array
     */
    public function getBingGongChangModule(array $params): array
    {
        return match ($params[&apos;position&apos;]) {
            WebSocketEnum::POSITION_FX_USER_DETAIL =&amp;gt; $this-&amp;gt;getBingGongChangFxUserPosition($params),
            default =&amp;gt; $this-&amp;gt;getBingGongChangIndexPosition($params),
        };
    }

    /**
     * 获取单兵工场模块数据
     * @param array $params
     * @return array
     */
    public function getSingleBingGongChangModule(array $params): array
    {
        return match ($params[&apos;position&apos;]) {
            WebSocketEnum::POSITION_FX_USER_DETAIL =&amp;gt; $this-&amp;gt;getBingGongChangFxUserPosition($params),
            default =&amp;gt; $this-&amp;gt;getSingleBingGongChangIndexPosition($params),
        };
    }

    /**
     * 获取机构模块数据
     * @param array $params
     * @return array
     */
    public function getInstitutionModule(array $params): array
    {
        return match ($params[&apos;position&apos;]) {
            WebSocketEnum::POSITION_INSTITUTION_PARTNER_DETAIL =&amp;gt; $this-&amp;gt;getInstitutionPartnerPosition($params),
            default =&amp;gt; $this-&amp;gt;getInstitutionIndexPosition($params),
        };
    }

    /**
     * 获取单机构模块数据
     * @param array $params
     * @return array
     */
    public function getSingleInstitutionModule(array $params): array
    {
        return match ($params[&apos;position&apos;]) {
            WebSocketEnum::POSITION_INSTITUTION_PARTNER_DETAIL =&amp;gt; $this-&amp;gt;getInstitutionPartnerPosition($params),
            default =&amp;gt; $this-&amp;gt;getSingleInstitutionIndexPosition($params),
        };
    }

    /**
     * 获取分销者实时数据汇总模块数据
     * @param array $params
     * @return array
     */
    public function getSingleFactoryFxUserModule(array $params): array
    {
        return $this-&amp;gt;getSingleFactoryFxUserIndexPosition($params);
    }

    /**
     * 获取机构招商数据详情
     * @param array $params
     * @return array
     */
    protected function getInstitutionPartnerPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        return [
            &apos;institution_partner_detail_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getInstitutionPartnerDetailBlock(),
        ];
    }

    /**
     * 机构首页数据
     * @param array $params
     * @return array
     */
    protected function getInstitutionIndexPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);
        // 汇总
        $orderCensusService = new OrderCensusBusiness();
        $orderCensusService-&amp;gt;params = $paramService;

        // 申样
        $sendSampleService = new SendSampleBusiness();
        $sendSampleService-&amp;gt;params = $paramService;

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        // 图表
        $orderChartService = new OrderChartBusiness();
        $orderChartService-&amp;gt;params = $paramService;


        // 领样数据
        $send_sample_data = [
            // 领样数
            &apos;current_send_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getSendSampleCount(), 0),
            // 申样发货数
            &apos;current_express_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getExpressSampleCount(), 0),
        ];

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; $orderCensusService-&amp;gt;getGmvBlock(),
            // 销售数据（左上）
            &apos;sales_block&apos; =&amp;gt; array_merge($orderCensusService-&amp;gt;getSalesBlock(), $send_sample_data),
            // 招商排行（左下）
            &apos;institution_partner_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getInstitutionPartnerRankBlock(),
            // 订单折线图（右上）
            &apos;sales_chart_block&apos; =&amp;gt; $orderChartService-&amp;gt;getLineChartBlock(),
            // 机构销售榜单（右下）
            &apos;institution_sales_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getInstitutionSalesRankBlock(),
            // 机构销售概况（中心饼图）
            &apos;institution_sales_pie_block&apos; =&amp;gt; $orderChartService-&amp;gt;getInstitutionSalesPieBlock(),
        ];
    }

    /**
     * 获取分销者实时数据汇总首页数据
     * @param array $params
     * @return array
     */
    protected function getSingleFactoryFxUserIndexPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 汇总
        $orderCensusService = new OrderCensusBusiness();
        $orderCensusService-&amp;gt;params = $paramService;

        // 申样
        $sendSampleService = new SendSampleBusiness();
        $sendSampleService-&amp;gt;params = $paramService;

        // 排行
        $orderRankService = new OrderRankBusiness();;
        $orderRankService-&amp;gt;params = $paramService;



        // 领样数据
        $send_sample_data = [
            // 领样数
            &apos;current_send_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getSendSampleCount(), 0),
            // 去重达人数
            &apos;current_distinct_talent_num&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getDistinctTalentNum(), 0),
        ];

        // 分销者总数
        $fx_user_count = $orderCensusService-&amp;gt;getFxUserCount();

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; array_merge($orderCensusService-&amp;gt;getGmvBlock(), compact(&apos;fx_user_count&apos;)),
            // 销售数据（左上）
            &apos;sales_block&apos; =&amp;gt; array_merge($orderCensusService-&amp;gt;getSalesBlock(), $send_sample_data),
            // 小组排行（左下）
            &apos;fx_user_group_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getFxUserGroupRankBlock(),
            // 分销者销售红榜（右上）
            &apos;fx_user_red_sales_block&apos; =&amp;gt; $orderRankService-&amp;gt;getCommonFxUserRankBlock(),
            // 分销者销售黑榜（右下）
            &apos;fx_user_black_sales_block&apos; =&amp;gt; $orderRankService-&amp;gt;getCommonFxUserRankBlock(&apos;ASC&apos;),
            // 分销者详情
            &apos;fx_user_detail_block&apos; =&amp;gt; $orderRankService-&amp;gt;getFxUserDetailBlock(),
        ];
    }


    /**
     * 单机构首页数据
     * @param array $params
     * @return array
     */
    protected function getSingleInstitutionIndexPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 汇总
        $orderCensusService = new OrderCensusBusiness();
        $orderCensusService-&amp;gt;params = $paramService;

        // 申样
        $sendSampleService = new SendSampleBusiness();
        $sendSampleService-&amp;gt;params = $paramService;

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        // 图表
        $orderChartService = new OrderChartBusiness();
        $orderChartService-&amp;gt;params = $paramService;

        // 领样数据
        $send_sample_data = [
            // 领样数
            &apos;current_send_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getSendSampleCount(), 0),
            // 申样发货数
            &apos;current_express_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getExpressSampleCount(), 0),
        ];

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; $orderCensusService-&amp;gt;getGmvBlock(),
            // 销售数据（左上）
            &apos;sales_block&apos; =&amp;gt; array_merge($orderCensusService-&amp;gt;getSalesBlock(), $send_sample_data),
            // 招商排行（左下）
            &apos;institution_partner_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getInstitutionPartnerRankBlock(),
            // 订单折线图（右上）
            &apos;sales_chart_block&apos; =&amp;gt; $orderChartService-&amp;gt;getLineChartBlock(),
            // 产品分类（右下）
            &apos;category_sales_pie_block&apos; =&amp;gt; $orderChartService-&amp;gt;getCategorySalesPieBlock(),
            // 兵工场销售占比（中心饼图）
            &apos;bing_factory_sales_pie_block&apos; =&amp;gt; $orderChartService-&amp;gt;getBingFactorySalesPieBlock(),
        ];
    }


    /**
     * 兵工场首页数据
     * @param array $params
     * @return array
     */
    protected function getBingGongChangIndexPosition(array $params): array
    {


        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 汇总
        $orderCensusService = new OrderCensusBusiness();
        $orderCensusService-&amp;gt;params = $paramService;

        // 申样
        $sendSampleService = new SendSampleBusiness();
        $sendSampleService-&amp;gt;params = $paramService;

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        // 图表
        $orderChartService = new OrderChartBusiness();
        $orderChartService-&amp;gt;params = $paramService;

        // 领样数据
        $send_sample_data = [
            // 领样数
            &apos;current_send_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getSendSampleCount(), 0),
            // 去重达人数
            &apos;current_distinct_talent_num&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getDistinctTalentNum(), 0),
        ];

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; $orderCensusService-&amp;gt;getGmvBlock(),
            // 销售数据（左上）
            &apos;sales_block&apos; =&amp;gt; array_merge($orderCensusService-&amp;gt;getSalesBlock(), $send_sample_data),
            // 分销者销售排行（左下）
            &apos;fx_user_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getFxUserRankBlock(),
            // 订单折线图（右上）
            &apos;sales_chart_block&apos; =&amp;gt; $orderChartService-&amp;gt;getLineChartBlock(),
            // 兵工场排行（右下）
            &apos;bing_factory_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getBingFactoryRankBlock(),
            // 兵工场地图
            &apos;bing_factory_map_block&apos; =&amp;gt; $orderChartService-&amp;gt;getBingFactoryMapBlock(),
        ];
    }


    /**
     * 单兵工场首页数据
     * @param array $params
     * @return array
     */
    protected function getSingleBingGongChangIndexPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 汇总
        $orderCensusService = new OrderCensusBusiness();
        $orderCensusService-&amp;gt;params = $paramService;

        // 申样
        $sendSampleService = new SendSampleBusiness();
        $sendSampleService-&amp;gt;params = $paramService;

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        // 图表
        $orderChartService = new OrderChartBusiness();
        $orderChartService-&amp;gt;params = $paramService;

        // 领样数据
        $send_sample_data = [
            // 领样数
            &apos;current_send_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getSendSampleCount(), 0),
            // 去重达人数
            &apos;current_distinct_talent_num&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getDistinctTalentNum(), 0),
        ];

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; $orderCensusService-&amp;gt;getGmvBlock(),
            // 销售数据（左上）
            &apos;sales_block&apos; =&amp;gt; array_merge($orderCensusService-&amp;gt;getSalesBlock(), $send_sample_data),
            // 分销者销售排行（左下）
            &apos;fx_user_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getFxUserRankBlock(),
            // 订单折线图（右上）
            &apos;sales_chart_block&apos; =&amp;gt; $orderChartService-&amp;gt;getLineChartBlock(),
            // 热销产品排行（右下）
            &apos;product_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getProductRankBlock(),
            // 机构销售占比
            &apos;institution_sales_pie_block&apos; =&amp;gt; $orderChartService-&amp;gt;getInstitutionSalesPieBlock(),
        ];
    }

    /**
     * 兵工场-分销者详情数据
     * @param array $params
     * @return array
     */
    protected function getBingGongChangFxUserPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        return [
            &apos;fx_user_detail_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getFxUserDetailBlock(),
        ];
    }


    /**
     * 分销者-分销者详情数据
     * @param array $params
     * @return array
     */
    protected function getFxUserDetailPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        return [
            &apos;fx_user_detail_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getFxUserDetailBlock(),
        ];
    }


    /**
     * 分销者首页数据
     * @param array $params
     * @return array
     */
    protected function getFxUserIndexPosition(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 汇总
        $orderCensusService = new OrderCensusBusiness();
        $orderCensusService-&amp;gt;params = $paramService;

        // 申样
        $sendSampleService = new SendSampleBusiness();
        $sendSampleService-&amp;gt;params = $paramService;

        // 排行
        $orderRankService = new OrderRankBusiness();
        $orderRankService-&amp;gt;params = $paramService;

        // 图表
        $orderChartService = new OrderChartBusiness();
        $orderChartService-&amp;gt;params = $paramService;

        // 领样数据
        $send_sample_data = [
            // 领样数
            &apos;current_send_sample_count&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getSendSampleCount(), 0),
            // 去重达人数
            &apos;current_distinct_talent_num&apos; =&amp;gt; formatWithUnit($sendSampleService-&amp;gt;getDistinctTalentNum(), 0),
        ];

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; $orderCensusService-&amp;gt;getGmvBlock(),
            // 销售数据（左上）
            &apos;sales_block&apos; =&amp;gt; array_merge($orderCensusService-&amp;gt;getSalesBlock(), $send_sample_data),
            // 分销者销售排行（左下）
            &apos;fx_user_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getFxUserRankBlock(),
            // 订单折线图（右上）
            &apos;sales_chart_block&apos; =&amp;gt; $orderChartService-&amp;gt;getLineChartBlock(),
            // 达人销售排行（右下）
            &apos;customers_sales_rank_block&apos; =&amp;gt; $orderRankService-&amp;gt;getCustomersSalesRankBlock(),
            // 地图
            &apos;fx_user_map_block&apos; =&amp;gt; $orderChartService-&amp;gt;getFxUserMapBlock(),
        ];
    }

    /**
     * 分销者模块
     * @param array $params
     * @return array
     */
    public function getFxUserModule(array $params): array
    {
        return match ($params[&apos;position&apos;]) {
            WebSocketEnum::POSITION_FX_USER_DETAIL =&amp;gt; $this-&amp;gt;getFxUserDetailPosition($params),
            default =&amp;gt; $this-&amp;gt;getFxUserIndexPosition($params),
        };
    }

    /**
     * 产品模块
     * @param array $params
     * @return array
     */
    public function getProductModule(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 产品
        $productService = new ProductBusiness();
        $productService-&amp;gt;params = $paramService;

        $orderChartService = new OrderChartBusiness();
        $orderChartService-&amp;gt;params = $paramService;

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; $productService-&amp;gt;getGmvBlock(),
            // 产品地图数据
            &apos;region_block&apos; =&amp;gt; $productService-&amp;gt;getRegionBlock(),
            // 产品数据汇总（左上）
            &apos;product_census_block&apos; =&amp;gt; $productService-&amp;gt;getProductCensusBlock(),
            // 出单产品增长情况（左下）
            &apos;product_sales_chart_block&apos; =&amp;gt; $productService-&amp;gt;getProductSalesChartBlock(),
            // 产品销售排行榜（右上）
            &apos;product_sales_rank_block&apos; =&amp;gt; $productService-&amp;gt;getProductSalesRandBlock(),
            // 产品分类占比（右下）
            &apos;product_category_chart_block&apos; =&amp;gt; $orderChartService-&amp;gt;getCategorySalesPieBlock(),
        ];
    }


    /**
     * 旺店通模块
     * @param array $params
     * @return array
     */
    public function getWdtModule(array $params): array
    {
        // 参数服务
        $paramService = new WebSocketParam();
        fillProperty($params, $paramService);

        // 旺店通订单
        $wdtOrderService = new WdtOriginOrderBusiness();
        $wdtOrderService-&amp;gt;params = $paramService;

        return [
            // gmv中心块
            &apos;gmv_block&apos; =&amp;gt; $wdtOrderService-&amp;gt;getGmvBlock(),
            // 店铺资金流向 中
            &apos;fund_flow_block&apos; =&amp;gt; $wdtOrderService-&amp;gt;getFundFlowBlock(),
            // 成交排行榜（左上）
            &apos;deal_rank_block&apos; =&amp;gt; $wdtOrderService-&amp;gt;getDealRankBlock(),
            // 订单监控 右上
            &apos;order_monitor_block&apos; =&amp;gt; $wdtOrderService-&amp;gt;getOrderMonitorBlock(),
            // 店铺销售占比 左下
            &apos;shop_sales_proportion_block&apos; =&amp;gt; $wdtOrderService-&amp;gt;getShopSalesProportionBlock(),
            // 仓库信息，右下
            &apos;warehouse_info_block&apos; =&amp;gt; $wdtOrderService-&amp;gt;getWarehouseInfoBlock(),
            // 仓库信息详情
//            &apos;warehouse_info_detail_block&apos; =&amp;gt; $wdtOrderService-&amp;gt;getWarehouseInfoDetailBlock(),
        ];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中使用到了Module/DataScreen/Common/Params/WebSocketParam.php,为了参数统一&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Module\DataScreen\Common\Params;

class WebSocketParam
{
    // 平台
    public int $platform;
    // 模块
    public int $module;
    // 开始时间
    public int $start_time;
    // 结束时间
    public int $end_time;
    // 开始时间 字符
    public string $date_start;
    // 结束时间 字符
    public string $date_end;
    // 时间类型
    public int $date_type;
    // 时间值
    public int $date_value;
    // 当前位置
    public int $position;
    // 分销者详情开始时间
    public int $fx_user_start_time;
    // 分销者详情结束时间
    public int $fx_user_end_time;
    // 分销者详情开始时间 字符
    public string $fx_user_date_start;
    // 分销者详情结束时间 字符
    public string $fx_user_date_end;
    // 分销者详情编号
    public string $fx_user_no;
    // 分销者详情页码
    public int $fx_user_page;

    // 模块相关用户ID
    public string $module_user_id = &apos;&apos;;

    // 用户ID
//    public int $user_id;

    // 数据权限ID
    public string $rule_source_ids = &apos;&apos;;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;使用到了助手函数 填充类属性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;if (!function_exists(&apos;fillProperty&apos;)) {
    /**
     * 使用反射填充属性
     * @param array $params
     * @param object $instance
     */
    function fillProperty(array $params, object $instance): void
    {
        $ref = new ReflectionObject($instance);
        foreach ($params as $key =&amp;gt; $value) {
            if ($ref-&amp;gt;hasProperty($key)) {
                $prop = $ref-&amp;gt;getProperty($key);
                $prop-&amp;gt;setAccessible(true); // 允许访问 protected/private
                $prop-&amp;gt;setValue($instance, $value);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. 测试&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;请求ws://127.0.0.1:9503/wsDataScreen/ws测试连接是否成功&lt;/p&gt;
&lt;p&gt;由于做了jwt鉴权需要带上token参数，目前我这里token不需要过期&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我这里的发送参数类似这样，根据不同模块参数值变化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;module&quot;:8,
    &quot;platform&quot;:0,
    &quot;date_type&quot;:1,
    &quot;date_value&quot;:0, 
    &quot;date_start&quot;:&quot;2025-08-12&quot;,
    &quot;date_end&quot;:&quot;2025-09-10&quot;,
    &quot;position&quot;:0, 
    &quot;fx_user_date_start&quot;:&quot;2025-02-12&quot;,
    &quot;fx_user_date_end&quot;:&quot;2025-03-10&quot;,
    &quot;fx_user_page&quot;:1,
    &quot;fx_user_no&quot;:&quot;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9. 创建定时任务&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;创建Command/DataScreenPushCommand.php文件 定时查询数据是否变化 推送对应客户端&lt;/p&gt;
&lt;p&gt;这里注意：手动运行command会推送失败 因为命令行的生命周期不一样，没有启动对应的websocket服务，定时任务可以正常推送。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

declare(strict_types=1);

namespace App\Command;

use App\Service\DataScreenPushService;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Command\Annotation\Command;
use Hyperf\Crontab\Annotation\Crontab;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Hyperf\WebSocketServer\Sender;

#[Command]
#[Crontab(
    rule: &quot;*\/1 * * * *&quot;,
    name: &quot;dataScreenPush&quot;,
    type: &quot;command&quot;,
    singleton: true,
    callback: [
        &apos;command&apos; =&amp;gt; &apos;dataScreen:push&apos;,
        &apos;--disable-event-dispatcher&apos; =&amp;gt; true,
    ],
    memo: &quot;数据大屏消息推送&quot;,
    enable: false,
)]
class DataScreenPushCommand extends HyperfCommand
{
    #[Inject]
    protected Sender $sender;
    public function __construct(
        protected ContainerInterface $container,
        protected DataScreenPushService $service,
        protected LoggerInterface $logger
    )
    {
        parent::__construct(&apos;dataScreen:push&apos;);
    }

    public function configure()
    {
        parent::configure();
        $this-&amp;gt;setDescription(&apos;数据大屏消息推送&apos;);
    }

    public function handle()
    {
        try {
            $start = microtime(true);

            $this-&amp;gt;service-&amp;gt;pushDataScreen();

            $duration = round(microtime(true) - $start, 2);
            $this-&amp;gt;info(&quot;数据大屏消息推送操作完成，耗时：{$duration}秒&quot;);
        } catch (\Throwable $e) {
            // 简单记录错误
            $this-&amp;gt;logger-&amp;gt;error(&apos;数据大屏消息推送系统错误&apos;, [
                &apos;error&apos; =&amp;gt; $e-&amp;gt;getMessage(),
                &apos;file&apos; =&amp;gt; $e-&amp;gt;getFile(),
                &apos;line&apos; =&amp;gt; $e-&amp;gt;getLine()
            ]);
            $this-&amp;gt;info(&apos;数据大屏消息推送系统错误！&apos; . var_export($e-&amp;gt;getMessage(), true));
        }

    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;创建服务Service/DataScreenPushService.php&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\Service;

use App\Common\Trait\RedisKeyTrait;
use App\Module\DataScreen\Common\Trait\DataCenterTrait;
use App\Module\DataScreen\Enum\RedisKeyEnum;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Hyperf\WebSocketServer\Sender;
use Psr\Log\LoggerInterface;

class DataScreenPushService
{
    use RedisKeyTrait;
    use DataCenterTrait;

    // 客户端=&amp;gt;筛选的key
    protected string $filter_key;

    // 存活客户端的key
    protected string $alive_key;

    #[Inject]
    private Redis $redis;

    #[Inject]
    private LoggerInterface $logger;

    #[Inject]
    private Sender $sender;
    public function pushDataScreen(): void
    {
        $this-&amp;gt;filter_key = $this-&amp;gt;generateKeyPrefix(RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD_FILTER);
        $this-&amp;gt;alive_key = $this-&amp;gt;generateKeyPrefix(RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD, version: 0);
        // 处理redis数据
        $fds = $this-&amp;gt;dealRedisData();

        if (empty($fds)) {
            $this-&amp;gt;logger-&amp;gt;error(&apos;没有存活的客户端&apos;);
            return;
        }

        try {
            $filter_group_version = [];
            $filter_group_map_need_push = [];
            foreach ($fds as $fd) {
                $fd = intval($fd);
                if (!$this-&amp;gt;sender-&amp;gt;check($fd)) {
                    $this-&amp;gt;redis-&amp;gt;sRem($this-&amp;gt;alive_key, $fd);
                    $this-&amp;gt;redis-&amp;gt;hDel($this-&amp;gt;filter_key, $fd);
                    continue;
                }

                // 获取筛选条件
                $params_raw = $this-&amp;gt;redis-&amp;gt;hGet($this-&amp;gt;filter_key, strval($fd));
                $params = json_decode($params_raw ?: &apos;&apos;, true);
                if (empty($params)) {
                    $this-&amp;gt;logger-&amp;gt;error(&quot;客户端{$fd}没有筛选条件&quot;);
                    continue;
                }

                // 参数转换字符串
                $other_key = $this-&amp;gt;generateFilterGroupKey($params);

                // 查询该筛选下的数据
                $filter_group_key = $this-&amp;gt;generateKeyPrefix(
                    RedisKeyEnum::REDIS_KEY_WEBSOCKET_FD_FILTER_GROUP,
                    RedisKeyEnum::MODULE_DATA_SCREEN,
                    &quot;:{$other_key}&quot;);
                $last_data_raw = $this-&amp;gt;redis-&amp;gt;get($filter_group_key);
                $last_data = json_decode($last_data_raw ?: &apos;&apos;, true) ?: [];

                // 当前版本号
                $last_version = ($last_data[&apos;version&apos;] ?? 0) ?: 0;
                !isset($filter_group_version[$filter_group_key]) &amp;amp;&amp;amp; $filter_group_version[$filter_group_key] = $last_version;

                if ($filter_group_version[$filter_group_key] &amp;lt; $last_version) {
                    // 该筛选查询过了且数据有变化 则直接推送
                    if (!empty($filter_group_map_need_push[$filter_group_key] ?? 0)) {
                        $this-&amp;gt;pushHandler($fd, $last_data[&apos;data&apos;] ?: [], $params);
                    }
                    continue;
                }

                // 查询数据
                try {
                    $data = $this-&amp;gt;dataCenter($params) ?: [];
                } catch (\Throwable $e) {
                    $this-&amp;gt;logger-&amp;gt;error(&apos;查询数据异常：参数为：&apos; . json_encode($params) . &apos;，异常原因：&apos; . $e-&amp;gt;getMessage());
                    continue;
                }

                $this-&amp;gt;redis-&amp;gt;set($filter_group_key, json_encode([&apos;filter&apos; =&amp;gt; $params, &apos;data&apos; =&amp;gt; $data, &apos;version&apos; =&amp;gt; $filter_group_version[$filter_group_key] + 1, &apos;update_time&apos; =&amp;gt; time()]));
                $this-&amp;gt;redis-&amp;gt;expire($filter_group_key, 7200);

                if (($last_data[&apos;data&apos;] ?? []) != $data) {
                    $filter_group_map_need_push[$filter_group_key] = 1;
                    $this-&amp;gt;pushHandler($fd, $data, $params);
                }
            }
        } catch (\Throwable $e) {
            $this-&amp;gt;logger-&amp;gt;error(&apos;数据大屏消息推送消息异常：&apos; . $e-&amp;gt;getMessage());
        }
    }

    /**
     * 推送消息
     * @param int $fd
     * @param array $data
     * @param array $last_filter_param
     * @return bool
     * @throws \RedisException
     */
    protected function pushHandler(int $fd, array $data, array $last_filter_param): bool
    {
        // 检查客户端是否在线
        if (!$this-&amp;gt;sender-&amp;gt;check($fd)) {
            $this-&amp;gt;redis-&amp;gt;sRem($this-&amp;gt;alive_key, $fd);
            $this-&amp;gt;redis-&amp;gt;hDel($this-&amp;gt;filter_key, $fd);
            return false;
        }

        // 再次检查筛选条件是否变化
        $params_raw = $this-&amp;gt;redis-&amp;gt;hGet($this-&amp;gt;filter_key, strval($fd));
        $params = json_decode($params_raw ?: &apos;&apos;, true);
        if ($params != $last_filter_param) {
            return false;
        }

        // 推送消息
        return $this-&amp;gt;sender-&amp;gt;push($fd, responseSuccess($data));
    }

    /**
     * 对比数据不同处
     * @param array $last_data
     * @param array $data
     * @return array
     */
    protected function compareData(array $last_data, array $data): array
    {
        if (empty($last_data) || empty($data)) {
            return $data;
        }

        $return_data = [];
        foreach ($data as $key =&amp;gt; $value) {
            if (!isset($last_data[$key])) {
                $return_data[$key] = $value;
                continue;
            }
            if ($value != $last_data[$key]) {
                $return_data[$key] = $value;
            }
        }
        return $return_data;
    }

    /**
     * 处理redis数据
     * @return array
     */
    protected function dealRedisData(): array
    {
        try {
            // 存活的fd
            $fds = $this-&amp;gt;getAliveFds();

            // 客户端对应的筛选
            if (empty($fds)) {
                $this-&amp;gt;redis-&amp;gt;del($this-&amp;gt;filter_key);
                return [];
            }

            // 获取所有key
            $all_filter_fd = $this-&amp;gt;redis-&amp;gt;hKeys($this-&amp;gt;filter_key);
            foreach ($all_filter_fd as $fd_item) {
                if (!in_array($fd_item, $fds)) {
                    // 删除不存在的fd
                    $this-&amp;gt;redis-&amp;gt;hDel($this-&amp;gt;filter_key, $fd_item);
                }
            }
            return $fds;
        } catch (\Throwable $e) {
            $this-&amp;gt;logger-&amp;gt;error(&quot;操作redis出现错误：&quot; . $e-&amp;gt;getMessage());
            return [];
        }

    }

    /**
     * 获取存活的客户端
     * @return array
     * @throws \RedisException
     */
    protected function getAliveFds(): array
    {
        return $this-&amp;gt;redis-&amp;gt;sMembers($this-&amp;gt;alive_key) ?: [];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;使用到了返回函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;if (!function_exists(&apos;responseSuccess&apos;)) {
    /**
     * 成功响应
     */
    function responseSuccess(mixed $data = null, string $message = &apos;success&apos;, int $code = 200): false|string
    {
        return json_encode([
            &apos;code&apos; =&amp;gt; $code,
            &apos;message&apos; =&amp;gt; $message,
            &apos;data&apos; =&amp;gt; $data,
            &apos;timestamp&apos; =&amp;gt; time()
        ]);
    }
}

if (!function_exists(&apos;responseError&apos;)) {
    /**
     * 失败响应
     */
    function responseError(string $message = &apos;&apos;, int $code = 400, mixed $data = null): false|string
    {
        return json_encode([
            &apos;code&apos; =&amp;gt; $code,
            &apos;message&apos; =&amp;gt; $message ?: default_error_message($code),
            &apos;data&apos; =&amp;gt; $data,
            &apos;timestamp&apos; =&amp;gt; time()
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10. 反向代理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;配置nginx反向代理 顺便升级w s为w s s&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# 至少需要一个 Hyperf 节点，多个配置多行
upstream hyperf_websocket {
    # Hyperf WebSocket Server 的 IP 及 端口
    server 127.0.0.1:9503;
}

server {
    listen 80;
    listen 443 ssl;
    # 改成你的域名 可以匹配多个
    server_name xxx.com;
    # 配置 ssl  替换成自己的已经获得的域名签名文件名
    ssl_certificate     xxx.pem;  # pem文件的路径
    ssl_certificate_key  xxx.pem; # key文件的路径
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_tickets on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    add_header Strict-Transport-Security &quot;max-age=31536000&quot;;
    error_page 497  https://$host$request_uri;
    
    location / {
        # WebSocket Header
        proxy_http_version 1.1;
        proxy_set_header Upgrade websocket;
        proxy_set_header Connection &quot;Upgrade&quot;;
        
        # 将客户端的 Host 和 IP 信息一并转发到对应节点  
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
    
        # 客户端与服务端无交互 60s 后自动断开连接，请根据实际业务场景设置
        proxy_read_timeout 60s ;
        
        # 执行代理访问真实服务器
        proxy_pass http://hyperf_websocket;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;配置好之后可以使用wss://xxx.com/wsDataScreen/ws连接了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;11. 扩展&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;可以安装websocket客户端测试，非必要&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;composer require hyperf/websocket-client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建控制器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare(strict_types=1);

namespace App\Controller;

use Hyperf\Di\Annotation\Inject;
use Hyperf\WebSocketClient\ClientFactory;
use Hyperf\WebSocketClient\Frame;

class IndexController
{
    #[Inject]
    protected ClientFactory $clientFactory;

    public function index()
    {
        // 对端服务的地址，如没有提供 ws:// 或 wss:// 前缀，则默认补充 ws://
        $host = &apos;127.0.0.1:9503&apos;;
        // 通过 ClientFactory 创建 Client 对象，创建出来的对象为短生命周期对象
        $client = $this-&amp;gt;clientFactory-&amp;gt;create($host);
        // 向 WebSocket 服务端发送消息
        $client-&amp;gt;push(&apos;HttpServer 中使用 WebSocket Client 发送数据。&apos;);
        // 获取服务端响应的消息，服务端需要通过 push 向本客户端的 fd 投递消息，才能获取；以下设置超时时间 2s，接收到的数据类型为 Frame 对象。
        /** @var Frame $msg */
        $msg = $client-&amp;gt;recv(2);
        // 获取文本数据：$res_msg-&amp;gt;data
        return $msg-&amp;gt;data;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;12. 总结&lt;/h3&gt;
&lt;p&gt;该流程会在握手阶段把存活的客户端fd保存在redis的DataScreen_prod_websocket_active_fd中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据类似&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;1161
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后记录客户端的筛选条件到redis的DataScreen_prod_websocket_fd_filter_v1中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据类似 3209对应&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;date_end&quot;: &quot;2025-09-26&quot;,
    &quot;date_start&quot;: &quot;2025-09-25&quot;,
    &quot;date_type&quot;: 1,
    &quot;date_value&quot;: 0,
    &quot;end_time&quot;: 1758902399,
    &quot;fx_user_date_end&quot;: &quot;&quot;,
    &quot;fx_user_date_start&quot;: &quot;&quot;,
    &quot;fx_user_end_time&quot;: 0,
    &quot;fx_user_no&quot;: &quot;&quot;,
    &quot;fx_user_page&quot;: 1,
    &quot;fx_user_start_time&quot;: 0,
    &quot;module&quot;: 8,
    &quot;module_user_id&quot;: &quot;&quot;,
    &quot;platform&quot;: 1,
    &quot;position&quot;: 0,
    &quot;rule_source_ids&quot;: &quot;&quot;,
    &quot;start_time&quot;: 1758729600
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次查询都会记录数据缓存到&lt;code&gt;DataScreen_prod_websocket_fd_filter_group:date_end__date_start__date_type_0_date_value_1_end_time_1762790399_fx_user_date_end__fx_user_date_start__fx_user_end_time_0_fx_user_no__fx_user_page_1_fx_user_start_time_0_module_8_module_user_id__platform_1_position_0_rule_source_ids__start_time_1762704000_v1&lt;/code&gt;hash中，为命令行校验数据变动使用，同时也加快了查询效率。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据类似&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;filter&quot;: {
        &quot;date_end&quot;: &quot;&quot;,
        &quot;date_start&quot;: &quot;&quot;,
        &quot;date_type&quot;: 0,
        &quot;date_value&quot;: 1,
        &quot;end_time&quot;: 1762790399,
        &quot;fx_user_date_end&quot;: &quot;&quot;,
        &quot;fx_user_date_start&quot;: &quot;&quot;,
        &quot;fx_user_end_time&quot;: 0,
        &quot;fx_user_no&quot;: &quot;&quot;,
        &quot;fx_user_page&quot;: 1,
        &quot;fx_user_start_time&quot;: 0,
        &quot;module&quot;: 8,
        &quot;module_user_id&quot;: &quot;&quot;,
        &quot;platform&quot;: 1,
        &quot;position&quot;: 0,
        &quot;rule_source_ids&quot;: &quot;&quot;,
        &quot;start_time&quot;: 1762704000
    },
    &quot;data&quot;: {},
    &quot;version&quot;: 1,
    &quot;update_time&quot;: 1762763257
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>🍡mac下部署laradock环境</title><link>https://www.corgi37.top/posts/mac-xia-bu-shu-laradock-huan-jing/</link><guid isPermaLink="true">https://www.corgi37.top/posts/mac-xia-bu-shu-laradock-huan-jing/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;项目结构&lt;/h2&gt;
&lt;p&gt;该目录位于~下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;program
├── develop
    ├── laradock(非手动创建)
    └── www
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;拉取项目&lt;/h2&gt;
&lt;p&gt;在～/program/develop下，从 github 上拉取 laradock 项目&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/laradock/laradock.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果遇到网络问题 可以设置代理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#设置http代理
git config --global https.proxy http://代理:xxxx
#设置https代理
git config --global https.proxy http://代理:xxxx
#取消http代理
git config --global --unset http.proxy
#取消https代理
git config --global --unset https.proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入～/program/develop/laradock目录将 &lt;code&gt;.env-example&lt;/code&gt; 重命名为 &lt;code&gt;.env&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp .env.example .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.env&lt;/code&gt;文件需要修改的地方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#这个是用于指定你的应用程序的目录，默认是在 Laradock 的上一层:
APP_CODE_PATH_HOST=../www
#php版本 按需修改
PHP_VERSION=8.3
#mysql版本 按需修改
MYSQL_VERSION=8.4

#在文件尾部加入代理地址
HTTP_PROXY=代理:xxxx
HTTPS_PROXY=代理:xxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入～/program/develop/workspace 修改&lt;code&gt;Dockerfile&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#在FROM关键字后加入配置
ENV HTTP_PROXY=&quot;http://代理:xxxx&quot;
#curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash这一行会报错，依旧是网络问题
#以下两种方式都可以 任选一种 或者在本机下载下来后传到oss 更改下载链接
curl --proxy 代理:xxxx -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
curl -x 代理:xxxx -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回到～/program/develop/laradock原神启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d nginx php-fpm mysql redis workspace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原神启动成功！！&lt;/p&gt;
&lt;p&gt;每次启动的命令太长了且不好记 所以可以使用shell脚本启动&lt;/p&gt;
&lt;p&gt;将&lt;code&gt;laradock.sh&lt;/code&gt;放到任意位置 可以是~/下 也可以是~/program/下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

cd ~/program/develop/laradock

print_style() {
  if [ &quot;$2&quot; == &quot;info&quot; ]; then
    COLOR=&quot;96m&quot;
  elif [ &quot;$2&quot; == &quot;success&quot; ]; then
    COLOR=&quot;92m&quot;
  elif [ &quot;$2&quot; == &quot;warning&quot; ]; then
    COLOR=&quot;93m&quot;
  elif [ &quot;$2&quot; == &quot;danger&quot; ]; then
    COLOR=&quot;91m&quot;
  else
    COLOR=&quot;0m&quot;
  fi

  STARTCOLOR=&quot;\e[$COLOR&quot;
  ENDCOLOR=&quot;\e[0m&quot;

  printf &quot;$STARTCOLOR%b$ENDCOLOR&quot; &quot;$1&quot;
}

display_options() {
  printf &quot;Available options:\n&quot;
  print_style &quot;   up&quot; &quot;success&quot;
  printf &quot;\t Starts containers\n&quot;
  print_style &quot;   down&quot; &quot;success&quot;
  printf &quot;\t Stops containers\n&quot;
  print_style &quot;   bash&quot; &quot;success&quot;
  printf &quot;\t Opens bash on the workspace with user laradock.\n&quot;
}

if [[ $# -eq 0 ]]; then
  print_style &quot;Missing arguments.\n&quot; &quot;danger&quot;
  display_options
  exit 1
fi

if [ &quot;$1&quot; == &quot;up&quot; ]; then
  if [ $# -gt 1 ]; then
    shift
    SERVICES=&quot;$*&quot;
  else
    SERVICES=&quot;nginx php-fpm mysql redis workspace&quot;
  fi
  print_style &quot;Starting Docker Compose\n&quot; &quot;info&quot;
  print_style &quot;Your services: ${SERVICES}\n&quot; &quot;success&quot;
  docker-compose up -d ${SERVICES}

elif [ &quot;$1&quot; == &quot;stop&quot; ]; then
  print_style &quot;Stopping Docker Compose\n&quot; &quot;info&quot;
  docker-compose stop

elif [ &quot;$1&quot; == &quot;down&quot; ]; then
  print_style &quot;Downing Docker Compose\n&quot; &quot;info&quot;
  docker-compose down

elif [ &quot;$1&quot; == &quot;bash&quot; ]; then
  docker-compose exec --user=laradock workspace bash

else
  print_style &quot;Invalid arguments.\n&quot; &quot;danger&quot;
  display_options
  exit 1
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从此以后你的启停命令就变成了以下几个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 不添加参数将默认启动 nginx, mysql, php-fpm, redis 容器
bash ./laradock.sh up

# 也可以根据需要设置需要启动的容器
bash ./laradock.sh up nginx phpmyadmin

# 停止容器
bash ./laradock.sh stop

# 进入 laradock workspace
bash ./laradock.sh bash
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>🍶ThinkPHP8中使用RabbitMQ</title><link>https://www.corgi37.top/posts/thinkphp8-zhong-shi-yong-rabbitmq/</link><guid isPermaLink="true">https://www.corgi37.top/posts/thinkphp8-zhong-shi-yong-rabbitmq/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;实现了消息消费失败入mysql数据库，提供了消息重新入队的command，有手动重试机制、防止消息重复消费逻辑。&lt;/p&gt;
&lt;p&gt;暂未实现延时队列&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.进入到项目根目录安装rabbitmq包&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;composer require php-amqplib/php-amqplib
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如需忽略版本安装 --ignore-platform-reqs，例如：&lt;code&gt;composer require --ignore-platform-reqs php-amqplib/php-amqplib&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.在&lt;code&gt;.env&lt;/code&gt;配置文件加入以下配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Rabbitmq
RABBITMQ_HOST = 127.0.0.1
RABBITMQ_PORT = 5672
RABBITMQ_LOGIN = admin
RABBITMQ_PASSWORD = admin
RABBITMQ_VHOST = /
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 在config文件夹下创建rabbitmq.php配置文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$env = app(&apos;env&apos;)-&amp;gt;get(&apos;app_env&apos;) ?? &apos;dev&apos;;
return [
    // 默认配置项
    &apos;default&apos; =&amp;gt; [
        &apos;host&apos; =&amp;gt; env(&apos;RABBITMQ_HOST&apos;, &apos;127.0.0.1&apos;),
        &apos;port&apos; =&amp;gt; env(&apos;RABBITMQ_PORT&apos;, 5672),
        &apos;login&apos; =&amp;gt; env(&apos;RABBITMQ_LOGIN&apos;, &apos;admin&apos;),
        &apos;password&apos; =&amp;gt; env(&apos;RABBITMQ_PASSWORD&apos;, &apos;admin&apos;),
        &apos;vhost&apos; =&amp;gt; env(&apos;RABBITMQ_VHOST&apos;, &apos;/&apos;),
    ],
    &apos;queues&apos; =&amp;gt; [
        // 测试直连队列
        &apos;hello&apos; =&amp;gt; [
            &apos;exchange&apos; =&amp;gt; &quot;test.hello.{$env}&quot;, // 交换机名称,不填表示使用默认的
            &apos;exchange_type&apos; =&amp;gt; &apos;direct&apos;, // 交换机类型 direct fanout topic headers
            &apos;queue&apos; =&amp;gt; &quot;test.hello.queue.{$env}&quot;, //队列名
            &apos;routing_key&apos; =&amp;gt; &quot;test.hello.key.{$env}&quot;, // 路由键
        ],
        // 测试广播队列
        &apos;promoter_bind_result&apos; =&amp;gt; [
            &apos;exchange&apos; =&amp;gt; &quot;promoter.bind.result.change.{$env}&quot;,
            &apos;exchange_type&apos; =&amp;gt; &apos;fanout&apos;,
            &apos;queue&apos; =&amp;gt; &quot;promoter.bind.result.change.queue.{$env}&quot;,
            &apos;routing_key&apos; =&amp;gt; &apos;&apos;,
        ]
    ],
];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.创建RabbitMQ帮助类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace core\Library;

use Exception;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Exception\AMQPConnectionClosedException;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Channel\AbstractChannel;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Wire\AMQPTable;
use think\facade\Config;
use think\facade\Log;

/**
 * Rabbitmq消息队列基类
 * Class Rabbitmq
 * @package core\Library
 */
class Rabbitmq
{
    /**
     * 连接
     * @var AMQPStreamConnection
     */
    private AMQPStreamConnection $connection;

    /**
     * 通道
     * @var AbstractChannel|AMQPChannel
     */
    private AbstractChannel|AMQPChannel $channel;

    public function __construct()
    {
        $this-&amp;gt;open();
    }

    public function __destruct()
    {
        $this-&amp;gt;close();
    }

    /**
     * 打开连接
     * @param string $config_name
     * @return void
     * @throws Exception
     */
    protected function open(string $config_name = &apos;default&apos;): void
    {
        if (!empty($this-&amp;gt;channel) &amp;amp;&amp;amp; $this-&amp;gt;channel-&amp;gt;is_open()) {
            return;
        }
        $config = Config::get(&quot;rabbitmq.{$config_name}&quot;);

        $this-&amp;gt;connection = new AMQPStreamConnection(
            $config[&apos;host&apos;],
            $config[&apos;port&apos;],
            $config[&apos;login&apos;],
            $config[&apos;password&apos;],
            $config[&apos;vhost&apos;],
            false,
            &apos;AMQPLAIN&apos;,
            null,
            &apos;en_US&apos;,
            60.0,
            60.0,
            null,
            false,
            30
        );

        $this-&amp;gt;channel = $this-&amp;gt;connection-&amp;gt;channel();

    }

    /**
     * 生产者发布消息
     * @param $message
     * @param string $queue 队列配置名称
     * @param string $message_id 指定消息ID
     * @return void
     * @throws Exception
     */
    public function sendMessage($message, string $queue = &apos;hello&apos;, string $message_id = &apos;&apos;): void
    {
        $this-&amp;gt;open();

        $config = Config::get(&quot;rabbitmq.queues.{$queue}&quot;);

        /**
         * 创建队列(Queue)
         * queue: hello         // 队列名称
         * passive: false      // 如果设置true存在则返回OK，否则就报错。设置false存在返回OK，不存在则自动创建
         * durable: true       // 是否持久化，设置false是存放到内存中RabbitMQ重启后会丢失,
         *                        设置true则代表是一个持久的队列，服务重启之后也会存在，因为服务会把持久化的Queue存放在硬盘上，当服务重启的时候，会重新加载之前被持久化的Queue
         * exclusive: false    // 是否排他，指定该选项为true则队列只对当前连接有效，连接断开后自动删除
         * auto_delete: false // 是否自动删除，当最后一个消费者断开连接之后队列是否自动被删除
         */
        $this-&amp;gt;channel-&amp;gt;queue_declare(
            $config[&apos;queue&apos;],
            false,
            true,
            false,
            false
        );

        /**
         * 创建交换机(Exchange)
         * exchange: exchange// 交换机名称
         * type: direct        // 交换机类型，分别为direct/fanout/topic
         * passive: false      // 如果设置true存在则返回OK，否则就报错。设置false存在返回OK，不存在则自动创建
         * durable: false      // 是否持久化，设置false是存放到内存中的，RabbitMQ重启后会丢失
         * auto_delete: false  // 是否自动删除，当最后一个消费者断开连接之后队列是否自动被删除
         */
        $this-&amp;gt;channel-&amp;gt;exchange_declare(
            $config[&apos;exchange&apos;],
            $config[&apos;exchange_type&apos;],
            false,
            true,
            false
        );

        /**
         * 绑定队列和交换机
         *  $queue 队列名称
         *  $exchange  交换器名称
         *  $routing_key   路由key
         */
        $this-&amp;gt;channel-&amp;gt;queue_bind($config[&apos;queue&apos;], $config[&apos;exchange&apos;], $config[&apos;routing_key&apos;]);

        $messageBody = is_array($message) ? json_encode($message, JSON_UNESCAPED_UNICODE) : $message;
        /**
         * 发送消息
         * messageBody:消息体
         * content_type:消息的类型 可以不指定
         * application_headers:公共头
         * delivery_mode:消息持久化最关键的参数
         * AMQPMessage::DELIVERY_MODE_NON_PERSISTENT = 1; 不持久化
         * AMQPMessage::DELIVERY_MODE_PERSISTENT = 2; 持久化
         */
        $msg = new AMQPMessage($messageBody, [
            &apos;content_type&apos; =&amp;gt; &apos;text/plain&apos;,
            &apos;delivery_mode&apos; =&amp;gt; AMQPMessage::DELIVERY_MODE_PERSISTENT,
            &apos;timestamp&apos; =&amp;gt; time(),
            &apos;application_headers&apos;=&amp;gt; new AMQPTable([
                &apos;retry&apos; =&amp;gt; 0, // 重试次数
                &apos;message_id&apos; =&amp;gt; $message_id ?: Snowflake::uuid(), // 消息唯一ID
                &apos;queue_config&apos; =&amp;gt; $queue,
                &apos;exchange_type&apos; =&amp;gt; $config[&apos;exchange_type&apos;],
                &apos;queue_name&apos; =&amp;gt; $config[&apos;queue&apos;],
            ]),
        ]);

        /**
         * 发送消息
         * msg       // AMQP消息内容
         * exchange  // 交换机名称
         * routing key     // 路由键名称
         */
        $this-&amp;gt;channel-&amp;gt;basic_publish($msg, $config[&apos;exchange&apos;], $config[&apos;routing_key&apos;]);

        $this-&amp;gt;close();
    }

    /**
     * 消费者接收消息
     * @param $callback
     * @param string $queue
     * @param bool $no_ack
     * @return void
     * @throws Exception
     */
    public function receiveMessage($callback, string $queue = &apos;hello&apos;, bool $no_ack = false): void
    {
        $this-&amp;gt;open();

        $config = Config::get(&quot;rabbitmq.queues.{$queue}&quot;);

        /**
         * 设置消费者（Consumer）客户端同时只处理一条队列
         * 这样是告诉RabbitMQ，再同一时刻，不要发送超过1条消息给一个消费者（Consumer），
         * 直到它已经处理了上一条消息并且作出了响应。这样，RabbitMQ就会把消息分发给下一个空闲的消费者（Consumer）。
         */
        $this-&amp;gt;channel-&amp;gt;basic_qos(0, 1, false);

        /**
         * 同样是创建路由和队列，以及绑定路由队列，注意要跟publisher的一致
         * 这里其实可以不用，但是为了防止队列没有被创建所以做的容错处理
         */
        $this-&amp;gt;channel-&amp;gt;queue_declare(
            $config[&apos;queue&apos;],
            false,
            true,
            false,
            false
        );
        $this-&amp;gt;channel-&amp;gt;exchange_declare(
            $config[&apos;exchange&apos;],
            $config[&apos;exchange_type&apos;],
            false,
            true,
            false
        );
        $this-&amp;gt;channel-&amp;gt;queue_bind($config[&apos;queue&apos;], $config[&apos;exchange&apos;], $config[&apos;routing_key&apos;]);

        /**
           queue: 从哪里获取消息的队列
           consumer_tag: 消费者标识符,用于区分多个客户端
           no_local: 不接收此使用者发布的消息
           no_ack: 设置为true，则使用者将使用自动确认模式。详情请参见.
                       自动ACK：消息一旦被接收，消费者自动发送ACK
                       手动ACK：消息接收后，不会发送ACK，需要手动调用
           exclusive:是否排他，即这个队列只能由一个消费者消费。适用于任务不允许进行并发处理的情况下
           nowait: 不返回执行结果，但是如果排他开启的话，则必须需要等待结果的，如果两个一起开就会报错
           callback: :回调逻辑处理函数,PHP回调 array($this, &apos;process_message&apos;) 调用本对象的process_message方法
       */

        $this-&amp;gt;channel-&amp;gt;basic_consume($config[&apos;queue&apos;], &apos;&apos;, false, $no_ack, false, false, $callback);

        register_shutdown_function(function () {
            $this-&amp;gt;close();
        });

        // 阻塞队列监听事件
        while ($this-&amp;gt;channel-&amp;gt;is_consuming()) {
            $this-&amp;gt;channel-&amp;gt;wait();
        }

        $this-&amp;gt;close();
    }



    /**
     * 关闭连接
     * @return void
     */
    protected function close(): void
    {
        if (empty($this-&amp;gt;channel)) {
            return;
        }
        $this-&amp;gt;channel-&amp;gt;close();
        try {
            $this-&amp;gt;connection-&amp;gt;close();
        } catch (\Throwable $e) {
            throw new AMQPConnectionClosedException(&quot;关闭连接失败！&quot;);
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中生成消息唯一ID有用到一个生成uuid的函数，也可以自己生成唯一数，主要是为了防止消息重复消费&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare(strict_types=1);

namespace core\Library;


class Snowflake
{

    // 开始时间戳（2023-01-01）
    const EPOCH = 1672531200000;

    // 机器ID所占位数
    const WORKER_ID_BITS = 5;

    // 数据中心ID所占位数
    const DATACENTER_ID_BITS = 5;

    // 序列号所占位数
    const SEQUENCE_BITS = 12;

    // 静态配置属性
    private static $isConfigured = false;
    private static $workerId = 1;
    private static $datacenterId = 1;
    private static $maxWorkerId;
    private static $maxDatacenterId;
    private static $workerIdShift;
    private static $datacenterIdShift;
    private static $timestampLeftShift;
    private static $sequenceMask;

    // 实例属性
    private $sequence = 0;
    private $lastTimestamp = -1;

    /**
     * 配置Snowflake参数
     * @param int $workerId 工作机器ID (0-31)
     * @param int $datacenterId 数据中心ID (0-31)
     * @throws \InvalidArgumentException
     */
    public static function config(int $workerId = 1, int $datacenterId = 1): void
    {
        self::$maxWorkerId = -1 ^ (-1 &amp;lt;&amp;lt; self::WORKER_ID_BITS);
        self::$maxDatacenterId = -1 ^ (-1 &amp;lt;&amp;lt; self::DATACENTER_ID_BITS);

        if ($workerId &amp;gt; self::$maxWorkerId || $workerId &amp;lt; 0) {
            throw new \InvalidArgumentException(&quot;Worker ID must be between 0 and &quot; . self::$maxWorkerId);
        }

        if ($datacenterId &amp;gt; self::$maxDatacenterId || $datacenterId &amp;lt; 0) {
            throw new \InvalidArgumentException(&quot;Datacenter ID must be between 0 and &quot; . self::$maxDatacenterId);
        }

        self::$workerId = $workerId;
        self::$datacenterId = $datacenterId;
        self::$workerIdShift = self::SEQUENCE_BITS;
        self::$datacenterIdShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS;
        self::$timestampLeftShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATACENTER_ID_BITS;
        self::$sequenceMask = -1 ^ (-1 &amp;lt;&amp;lt; self::SEQUENCE_BITS);

        self::$isConfigured = true;
    }

    /**
     * 生成雪花算法ID
     * @return string 64位整数字符串
     * @throws \RuntimeException
     */
    public function generate(): string
    {
        // 延迟初始化配置
        if (!self::$isConfigured) {
            self::config();
        }

        $timestamp = $this-&amp;gt;milliseconds();

        if ($timestamp &amp;lt; $this-&amp;gt;lastTimestamp) {
            $offset = $this-&amp;gt;lastTimestamp - $timestamp;
            if ($offset &amp;lt;= 5) { // 允许5ms的时钟回拨
                usleep($offset * 1000);
                $timestamp = $this-&amp;gt;milliseconds();
            } else {
                throw new \RuntimeException(&quot;Clock moved backwards. Refusing to generate id for {$offset} milliseconds&quot;);
            }
        }

        if ($this-&amp;gt;lastTimestamp == $timestamp) {
            $this-&amp;gt;sequence = ($this-&amp;gt;sequence + 1) &amp;amp; self::$sequenceMask;
            if ($this-&amp;gt;sequence == 0) {
                $timestamp = $this-&amp;gt;tilNextMillis($this-&amp;gt;lastTimestamp);
            }
        } else {
            $this-&amp;gt;sequence = random_int(0, 9); // 引入随机数降低冲突概率
        }

        $this-&amp;gt;lastTimestamp = $timestamp;

        return (string)(
            (($timestamp - self::EPOCH) &amp;lt;&amp;lt; self::$timestampLeftShift) |
            (self::$datacenterId &amp;lt;&amp;lt; self::$datacenterIdShift) |
            (self::$workerId &amp;lt;&amp;lt; self::$workerIdShift) |
            $this-&amp;gt;sequence
        );
    }

    /**
     * 静态方式生成ID
     * @return string
     * @throws \RuntimeException
     */
    public static function uuid(): string
    {
        return static::getInstance()-&amp;gt;generate();  // 使用 static 代替 self
    }

    /**
     * 获取当前毫秒时间戳
     * @return int
     */
    private function milliseconds(): int
    {
        return (int)(microtime(true) * 1000);
    }

    /**
     * 等待下一毫秒
     * @param int $lastTimestamp
     * @return int
     */
    private function tilNextMillis(int $lastTimestamp): int
    {
        $timestamp = $this-&amp;gt;milliseconds();
        while ($timestamp &amp;lt;= $lastTimestamp) {
            $timestamp = $this-&amp;gt;milliseconds();
        }
        return $timestamp;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.队列常量定义&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;该常量为config/rabbitmq.php下queues配置的键名&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\common\enum;

/**
 * 队列配置的键名
 * Class AmqpQueueEnum
 * @package app\common\enum
 */
class AmqpQueueEnum
{
    const STATUS_INIT = 0;
    const STATUS_PADDING = 1;
    const STATUS_SUCCESS = 2;
    const STATUS_FAILED = 3;
    const STATUS_DESC = [
        self::STATUS_INIT =&amp;gt; &apos;待处理&apos;,
        self::STATUS_PADDING =&amp;gt; &apos;处理中&apos;,
        self::STATUS_SUCCESS =&amp;gt; &apos;处理成功&apos;,
        self::STATUS_FAILED =&amp;gt; &apos;处理失败&apos;
    ];

    // 测试队列
    const TEST_QUEUE_NAME = &apos;hello&apos;;
    // 测试广播队列
    const PROMOTER_BIND_RESULT_QUEUE_NAME = &apos;promoter_bind_result&apos;;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.生产者测试&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;command代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\command\definition\producer;

use app\common\enum\AmqpQueueEnum;
use core\Library\Rabbitmq;
use think\annotation\Inject;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;

class TestProducerCommand extends Command
{

    protected function configure()
    {
        $this-&amp;gt;setName(&apos;producer:test_producer&apos;)
            -&amp;gt;setDescription(&apos;测试生产者&apos;)
            -&amp;gt;addArgument(&apos;task&apos;, null,  &apos;业务类型&apos;);

    }

    protected function execute(Input $input, Output $output)
    {
        $exitCode = 0;

        try {
            // 在执行前记录日志，第二个参数设置是否捕获输出
            $this-&amp;gt;beforeExecute($input, false); // 设置为 false 则不捕获输出

            // 业务逻辑
            $output-&amp;gt;writeln(&quot;command is start...&quot;);
            $task = $input-&amp;gt;getArgument(&apos;task&apos;);

            if (method_exists($this, $task)) {
                $this-&amp;gt;$task($output);
            } else {
                $output-&amp;gt;writeln(&quot;Task &apos;$task&apos; not found.&quot;);
            }
            $output-&amp;gt;writeln(&apos;successfully!&apos;);
        } catch (\Throwable $e) {
            dump($e);
            $exitCode = $e-&amp;gt;getCode() ?: 1;
            $this-&amp;gt;handleException($e);
            if (Db::connect()-&amp;gt;getPdo()-&amp;gt;inTransaction()) {
                Db::rollback();
            }
        } finally {
            $errorMessage = $this-&amp;gt;lastError ? $this-&amp;gt;lastError[&apos;message&apos;] : &apos;&apos;;
            if ($errorMessage) {
                $output-&amp;gt;writeln($errorMessage);
            }
            $this-&amp;gt;afterExecute($output, $exitCode, $errorMessage);
        }

        return $exitCode;
    }

    /**
     * @command php think producer:test_producer publishMessage
     * @taskname 生产者测试
     */
    protected function publishMessage(): void
    {
        try {
            $rabbitmq = new Rabbitmq();
            $rabbitmq-&amp;gt;sendMessage([&apos;a&apos; =&amp;gt; 1, &apos;b&apos; =&amp;gt; 2], AmqpQueueEnum::TEST_QUEUE_NAME);
        } catch (\Exception $e) {
            echo &apos;发生错误：&apos; . var_export($e-&amp;gt;getMessage(), true) . PHP_EOL;
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;配置好console文件（第12点），运行命令&lt;code&gt;php think producer:test_producer publishMessage&lt;/code&gt;后可以在rabbitmq面板上看到有test.hello.queue.dev队列生成了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7. 消费者测试&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;以下代码仅测试消费者是否正常 不包含重试、失败消息入库逻辑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\command\definition\consumer;

use app\common\enum\AmqpQueueEnum;
use core\Library\Rabbitmq;
use PhpAmqpLib\Exception\AMQPChannelClosedException;
use PhpAmqpLib\Exception\AMQPConnectionBlockedException;
use PhpAmqpLib\Exception\AMQPConnectionClosedException;
use PhpAmqpLib\Exception\AMQPIOException;
use PhpAmqpLib\Message\AMQPMessage;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;
use think\facade\Log;

class TestConsumerCommand extends Command
{

    protected function configure()
    {
        $this-&amp;gt;setName(&apos;consumer:test_consumer&apos;)
            -&amp;gt;setDescription(&apos;测试消费者&apos;)
            -&amp;gt;addArgument(&apos;task&apos;, null,  &apos;业务类型&apos;);

    }

    protected function execute(Input $input, Output $output)
    {
        $exitCode = 0;

        try {
            // 在执行前记录日志，第二个参数设置是否捕获输出
            $this-&amp;gt;beforeExecute($input, false); // 设置为 false 则不捕获输出

            // 业务逻辑
            $output-&amp;gt;writeln(&quot;command is start...&quot;);
            $task = $input-&amp;gt;getArgument(&apos;task&apos;);

            if (method_exists($this, $task)) {
                $this-&amp;gt;$task($output);
            } else {
                $output-&amp;gt;writeln(&quot;Task &apos;$task&apos; not found.&quot;);
            }
            $output-&amp;gt;writeln(&apos;successfully!&apos;);
        } catch (\Throwable $e) {
            dump($e);
            $exitCode = $e-&amp;gt;getCode() ?: 1;
            $this-&amp;gt;handleException($e);
            if (Db::connect()-&amp;gt;getPdo()-&amp;gt;inTransaction()) {
                Db::rollback();
            }
        } finally {
            $errorMessage = $this-&amp;gt;lastError ? $this-&amp;gt;lastError[&apos;message&apos;] : &apos;&apos;;
            if ($errorMessage) {
                $output-&amp;gt;writeln($errorMessage);
            }
            $this-&amp;gt;afterExecute($output, $exitCode, $errorMessage);
        }

        return $exitCode;
    }


    /**
     * @command php think consumer:test_consumer consumeMessage
     * @taskname 消费者测试
     */
    protected function consumeMessage(): void
    {
        $callback = function ($msg) {
            $this-&amp;gt;callbackFunction($msg);
        };

        // 常驻内存 消费消息
        while (true) {
            try {
                $rabbitmq = new Rabbitmq();
                // 不可使用[$this, &apos;callbackFunction&apos;]方式，该方式会报错 只能使用闭包函数
                $rabbitmq-&amp;gt;receiveMessage($callback, AmqpQueueEnum::TEST_QUEUE_NAME);

            } catch (\Exception $e) {
                sleep(5);
                Log::error(&quot;外部发生异常：&quot; . var_export($e-&amp;gt;getMessage(), true));
            }
        }
    }


    /**
     * 处理消息
     * @param AMQPMessage $msg
     * @return void
     */
    protected function callbackFunction(AMQPMessage $msg): void
    {
        try {
            $body = json_decode($msg-&amp;gt;getBody(), true);
            Log::error(&quot;接收到消息：&quot; . $msg-&amp;gt;getBody());
            $msg-&amp;gt;ack();
        } catch (AMQPIOException|AMQPConnectionClosedException|AMQPConnectionBlockedException|AMQPChannelClosedException|\Exception $e) {
            Log::error(&quot;发生异常：&quot; . var_export($e-&amp;gt;getMessage(), true));
            $msg-&amp;gt;reject(false);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. 创建失败消息记录表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tk_amqp_dead_letter_message` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `message_id` bigint unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;消息ID&apos;,
  `exchange` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT &apos;&apos; COMMENT &apos;原始交换机&apos;,
  `routing_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT &apos;&apos; COMMENT &apos;原始路由键&apos;,
  `queue_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT &apos;&apos; COMMENT &apos;队列名称&apos;,
  `queue_config` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT &apos;&apos; COMMENT &apos;队列配置名（区分业务）&apos;,
  `exchange_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT &apos;&apos; COMMENT &apos;交换机类型：direct fanout topic headers&apos;,
  `message_content` json NOT NULL COMMENT &apos;消息内容&apos;,
  `retry_count` int unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;重试次数&apos;,
  `last_retry_time` datetime DEFAULT NULL COMMENT &apos;最后重试时间&apos;,
  `status` tinyint unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;状态(0-待处理,1-处理中,2-处理成功,3-处理失败)&apos;,
  `error_msg` json DEFAULT NULL COMMENT &apos;错误信息&apos;,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT &apos;创建时间&apos;,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT &apos;更新时间&apos;,
  `delete_time` datetime DEFAULT NULL COMMENT &apos;删除时间&apos;,
  PRIMARY KEY (`id`),
  KEY `idxes_queue_config_status` (`queue_config`,`status`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB AUTO_INCREMENT=147826 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT=&apos;Amqp失败队列消息表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9.失败消息常量定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\common\enum;

class CacheEnum
{
    // 防止队列重复消费键
    const CHECK_QUEUE_REPEAT_CONSUMING_KEY = &apos;check_queue_repeat_consuming&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10. 生产可用消费者&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;command代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\command\definition\consumer;

use app\command\logic\ChangeNotifyLogic;
use app\common\enum\AmqpQueueEnum;
use PhpAmqpLib\Message\AMQPMessage;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;

class ChangeNotifyCommand extends Command
{

    protected function configure()
    {
        $this-&amp;gt;setName(&apos;consumer:change_notify&apos;)
            -&amp;gt;setDescription(&apos;消费者&apos;)
            -&amp;gt;addArgument(&apos;task&apos;, null,  &apos;业务类型&apos;);

    }

    protected function execute(Input $input, Output $output)
    {
        $exitCode = 0;

        try {
            // 在执行前记录日志，第二个参数设置是否捕获输出
            $this-&amp;gt;beforeExecute($input, false); // 设置为 false 则不捕获输出

            // 业务逻辑
            $output-&amp;gt;writeln(&quot;command is start...&quot;);
            $task = $input-&amp;gt;getArgument(&apos;task&apos;);

            if (method_exists($this, $task)) {
                $this-&amp;gt;$task($output);
            } else {
                $output-&amp;gt;writeln(&quot;Task &apos;$task&apos; not found.&quot;);
            }
            $output-&amp;gt;writeln(&apos;successfully!&apos;);
        } catch (\Throwable $e) {
            dump($e);
            $exitCode = $e-&amp;gt;getCode() ?: 1;
            $this-&amp;gt;handleException($e);
            if (Db::connect()-&amp;gt;getPdo()-&amp;gt;inTransaction()) {
                Db::rollback();
            }
        } finally {
            $errorMessage = $this-&amp;gt;lastError ? $this-&amp;gt;lastError[&apos;message&apos;] : &apos;&apos;;
            if ($errorMessage) {
                $output-&amp;gt;writeln($errorMessage);
            }
            $this-&amp;gt;afterExecute($output, $exitCode, $errorMessage);
        }

        return $exitCode;
    }


    /**
     * @command php think consumer:change_notify consumePromoterBindResult
     * @taskname 消费者
     */
    protected function consumePromoterBindResult(): void
    {
//        $msg = new AMQPMessage(json_encode([&apos;MsgType&apos; =&amp;gt; &apos;event&apos;, &apos;Event&apos; =&amp;gt; &apos;promoter_bind_result&apos;, &apos;bind_status&apos; =&amp;gt; 1, &apos;sharer_appid&apos; =&amp;gt; &apos;&apos;, &apos;sharer_openid&apos; =&amp;gt; &apos;&apos;, &apos;CreateTime&apos; =&amp;gt; 1750876217]));
//        ChangeNotifyLogic::getInstance()-&amp;gt;promoterBindResultConsume($msg);
        $option = [&apos;promoterBindResultConsume&apos;, AmqpQueueEnum::PROMOTER_BIND_RESULT_QUEUE_NAME];
        ChangeNotifyLogic::getInstance()-&amp;gt;changeConsume(...$option);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;业务层代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace app\command\logic;

use app\business\ProductDetailBusiness;
use app\common\enum\AmqpQueueEnum;
use app\common\enum\ChangeNotifyCallbackEnum;
use app\common\enum\SphProductEnum;
use app\repository\SphProductDetailRepository;
use app\repository\SphProductRepository;
use app\repository\SphUserSharerRepository;
use app\service\TkBoostService;
use core\Exception\App\BusinessException;
use core\Library\Rabbitmq;
use core\Trait\AmqpDlxTrait;
use core\Trait\SingletonTrait;
use PhpAmqpLib\Exception\AMQPChannelClosedException;
use PhpAmqpLib\Exception\AMQPConnectionBlockedException;
use PhpAmqpLib\Exception\AMQPConnectionClosedException;
use PhpAmqpLib\Exception\AMQPIOException;
use PhpAmqpLib\Message\AMQPMessage;
use think\facade\Config;
use think\facade\Db;
use think\facade\Log;

class ChangeNotifyLogic
{
    use SingletonTrait;
    use AmqpDlxTrait;

    /**
     * 开始监听队列消息
     * @param \Closure $callback 回调函数
     * @param string $queue_name 配置键名
     * @return void
     */
    protected function startListen(\Closure $callback, string $queue_name): void
    {
        // 常驻内存 消费消息
        while (true) {
            try {
                $rabbitmq = new Rabbitmq();
                // 不可使用[$this, &apos;callbackFunction&apos;]方式，该方式会报错 只能使用闭包函数
                $rabbitmq-&amp;gt;receiveMessage($callback, $queue_name);

            } catch (\Exception $e) {
                sleep(5);
                Log::error(&quot;[监听消息{$queue_name}]外部发生异常：&quot; . var_export($e-&amp;gt;getMessage(), true));
            }
        }
    }


    /**
     * 变更通知事件队列消费
     * @param string $callback_function_name 回调函数名称
     * @param string $queue_name 队列配置名
     * @return void
     */
    public function changeConsume(string $callback_function_name, string $queue_name): void
    {
        $callback = function ($msg) use ($callback_function_name) {
            $this-&amp;gt;$callback_function_name($msg);
        };

        $this-&amp;gt;startListen($callback, $queue_name);
    }


    /**
     * 消费者
     * @param AMQPMessage $msg
     * @return void
     */
    protected function promoterBindResultConsume(AMQPMessage $msg): void
    {
        try {
            Log::info(&quot;接收到消息：&quot; . $msg-&amp;gt;getBody());
            $body = json_decode($msg-&amp;gt;getBody(), true);

            if (empty($body)) {
                throw new BusinessException(&quot;消息不合法&quot;);
            }

            // 检查消息是否重复消费
            if (!$this-&amp;gt;checkMessageRepeat($msg)) {
                throw new BusinessException(&quot;[推客绑定机构的回调消费者]消息重复消费,本次跳过&quot;);
            }

            // todo 业务处理 ba la ba la

            // 确认应答
            $this-&amp;gt;ackHandle($msg);
            // 主动断开连接
            Db::close();
            // 适当延迟，避免接口限流
            usleep(200000); // 200ms
        } catch (BusinessException|AMQPIOException|AMQPConnectionClosedException|AMQPConnectionBlockedException|AMQPChannelClosedException|\Exception $e) {
            if ($e instanceof BusinessException) {
                Log::error(&quot;发生业务性异常 消息丢弃：&quot; . $msg-&amp;gt;getBody() . &quot;，错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true));
                $error_msg = &quot;[业务异常]错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true);
                // 丢弃消息
                $this-&amp;gt;rejectHandle($msg, $error_msg);
            } else {
                Log::error(&quot;发生致命异常 消息重新入队：&quot; . $msg-&amp;gt;getBody() . &quot;，错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true));
                $error_msg = &quot;[致命异常]错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true);
                // 手动重试机制
                $this-&amp;gt;retryHandle($msg, $error_msg);
            }

            // 主动断开连接
            Db::close();
            // 适当延迟，避免接口限流
            usleep(200000); // 200ms
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中用到单例模式的特征&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types = 1);

namespace core\Trait;

trait SingletonTrait
{
    private static $instance = null;  // 使用 nullable 类型并初始化为 null

    private function __construct() {}
    private function __clone(): void {}
    public function __wakeup(): void {
        throw new \RuntimeException(&quot;Cannot unserialize singleton&quot;);
    }

    public static function getInstance(): static
    {
        if (self::$instance === null) {
            self::$instance = new static();  // 使用 static 代替 self 以支持继承
        }

        return self::$instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;失败消息处理特征&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace core\Trait;

use app\common\enum\AmqpQueueEnum;
use app\common\enum\CacheEnum;
use app\repository\AmqpDeadLetterMessageRepository;
use core\Library\CacheHelper;
use core\Library\Snowflake;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
use think\facade\Cache;

trait AmqpDlxTrait
{
    /**
     * 手动重试
     * @param AMQPMessage $msg
     * @param string $error_msg 错误信息
     * @param int $retry_num 重试次数
     * @return bool
     */
    public function retryHandle(AMQPMessage $msg, string $error_msg, int $retry_num = 3): bool
    {
        $msgData = $this-&amp;gt;messageHandle($msg, $error_msg);
        $exchange      = $msgData[&apos;exchange&apos;];
        $routing_key   = $msgData[&apos;routing_key&apos;];
        $channel       = $msg-&amp;gt;getChannel();
        $body          = $msgData[&apos;message_content&apos;];
        $headersArray  = $msgData[&apos;headersArray&apos;];


        if ($headersArray[&apos;retry&apos;] &amp;lt; $retry_num) {
            $headersArray[&apos;retry&apos;]++;
            // 重新入队列
            $channel-&amp;gt;basic_publish(
                new AMQPMessage($body, [
                    &apos;content_type&apos; =&amp;gt; &apos;text/plain&apos;,
                    &apos;delivery_mode&apos; =&amp;gt; AMQPMessage::DELIVERY_MODE_PERSISTENT,
                    &apos;timestamp&apos; =&amp;gt; time(),
                    &apos;application_headers&apos;=&amp;gt; new AMQPTable($headersArray)
                ]),
                $exchange,
                $routing_key
            );
            // 因为重新入队了 所以这条消息直接确认
            $msg-&amp;gt;ack();
            $msgData[&apos;status&apos;] = AmqpQueueEnum::STATUS_PADDING;
        } else {
            // 丢弃
            $msg-&amp;gt;reject(false);
            $msgData[&apos;status&apos;] = AmqpQueueEnum::STATUS_FAILED;
        }
        // 记录失败日志
        return $this-&amp;gt;saveDlxMessage($msgData);
    }

    /**
     * 丢弃消息
     * @param AMQPMessage $msg
     * @param string $error_msg 错误信息
     * @return bool
     */
    public function rejectHandle(AMQPMessage $msg, string $error_msg): bool
    {
        $msgData = $this-&amp;gt;messageHandle($msg, $error_msg);
        $msgData[&apos;status&apos;] = AmqpQueueEnum::STATUS_FAILED;
        // 丢弃消息
        $msg-&amp;gt;reject(false);
        // 记录失败日志
        return $this-&amp;gt;saveDlxMessage($msgData);
    }

    /**
     * 确认应答
     * @param AMQPMessage $msg
     * @return bool
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function ackHandle(AMQPMessage $msg): bool
    {
        $msgData = $this-&amp;gt;messageHandle($msg, &apos;&apos;);
        $msgData[&apos;status&apos;] = AmqpQueueEnum::STATUS_SUCCESS;
        // 确认消息
        $msg-&amp;gt;ack();
        // 更新失败日志
        return $this-&amp;gt;saveDlxMessage($msgData, false);
    }

    /**
     * 获取队列消息详细信息
     * @param AMQPMessage $msg
     * @param string $error_msg
     * @param bool $needGenerateId 不存在message_id时是否生成
     * @return array
     */
    public function messageHandle(AMQPMessage $msg, string $error_msg, bool $needGenerateId = true): array
    {
        $exchange = $msg-&amp;gt;getExchange();
        $routing_key = $msg-&amp;gt;getRoutingKey();
        $message_content = $msg-&amp;gt;getBody();
        //headersObject 是一个AMQPTable对象
        $headersObject = $msg-&amp;gt;get_properties()[&apos;application_headers&apos;];
        //调用getNativeData()得到一个数组
        $headersArray  = $headersObject-&amp;gt;getNativeData();

        if ($needGenerateId &amp;amp;&amp;amp; !isset($headersArray[&apos;message_id&apos;])) {
            $headersArray[&apos;retry&apos;] = 0;
            $headersArray[&apos;message_id&apos;] = Snowflake::uuid();
        }

        $message_id = $headersArray[&apos;message_id&apos;];
        $queue_name = $headersArray[&apos;queue_name&apos;] ?? &apos;&apos;;
        $queue_config = $headersArray[&apos;queue_config&apos;] ?? &apos;&apos;;
        $exchange_type = $headersArray[&apos;exchange_type&apos;] ?? &apos;&apos;;
        $retry_count = ($headersArray[&apos;retry&apos;] ?? 0);
        $last_retry_time = date(&apos;Y-m-d H:i:s&apos;);
        $error_msg = compact(&apos;error_msg&apos;);

        return compact(&apos;exchange&apos;, &apos;routing_key&apos;, &apos;message_id&apos;, &apos;queue_name&apos;, &apos;error_msg&apos;,
            &apos;queue_config&apos;, &apos;exchange_type&apos;, &apos;retry_count&apos;, &apos;headersArray&apos;, &apos;message_content&apos;, &apos;last_retry_time&apos;);
    }

    /**
     * 检查消息是否重复消费
     * @return bool
     */
    public function checkMessageRepeat(AMQPMessage $msg): bool
    {
        $msgData = $this-&amp;gt;messageHandle($msg, &apos;&apos;, false);
        $message_id = intval($msgData[&apos;message_id&apos;]);
        // 重试的话不检查重复消费
        if (empty($message_id) || ($msgData[&apos;retry_count&apos;] ?? 0) &amp;gt; 0) {
            return true;
        }
        $redis_key = CacheEnum::CHECK_QUEUE_REPEAT_CONSUMING_KEY . &apos;:&apos; . app(&apos;env&apos;)-&amp;gt;get(&apos;app_env&apos;) . &quot;:{$message_id}&quot;;
        $redis = Cache::store(&apos;redis&apos;)-&amp;gt;handler();
        // 一天内不能重复消费
        return $redis-&amp;gt;setnx($redis_key, $message_id) &amp;amp;&amp;amp; $redis-&amp;gt;expire($redis_key, 24 * 3600);
    }

    /**
     * 保存失败队列消息
     * @param array $saveData
     * @param bool $needInsert 记录不存在是否新增
     * @return bool
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function saveDlxMessage(array $saveData, bool $needInsert = true): bool
    {
        if (empty($saveData) || empty($saveData[&apos;message_id&apos;])) {
            return false;
        }
        $saveWhere = [
            &apos;message_id&apos; =&amp;gt; $saveData[&apos;message_id&apos;],
            &apos;queue_config&apos; =&amp;gt; $saveData[&apos;queue_config&apos;],
        ];

        $saveData[&apos;update_time&apos;] = date(&apos;Y-m-d H:i:s&apos;);
       // 这里可以修改成自己的插入或更新tk_amqp_dead_letter_message表的方法
        return AmqpDeadLetterMessageRepository::getInstance()-&amp;gt;insertOrUpdate($saveWhere, $saveData, $needInsert);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;插入或更新方法类似下面这样&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public function insertOrUpdate(array $where, array $data, bool $needInsert = true): bool
{
    $this-&amp;gt;model = $this-&amp;gt;model-&amp;gt;newInstance();
    // 如果传入了 $where 条件，尝试查找记录
    if (!empty($where)) {
        $record = $this-&amp;gt;model-&amp;gt;where($where)-&amp;gt;find();
        // 记录存在，执行更新
        return $record &amp;amp;&amp;amp; $record-&amp;gt;save($data);
    }

    // 记录不存在，执行新增
    if ($needInsert) {
        return $this-&amp;gt;model-&amp;gt;save($data);
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;11.失败消息重新入队命令&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;command代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\command\definition\producer;

use app\command\logic\DeadLetterMessageLogic;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;

class DeadLetterMessageCommand extends Command
{

    // 消息唯一ID列表
    protected string $message_ids = &apos;&apos;;
    // 是否调试模式 0否 1是
    protected string $debug = &apos;0&apos;;

    protected function configure()
    {
        $this-&amp;gt;setName(&apos;producer:dead_letter_message&apos;)
            -&amp;gt;setDescription(&apos;失败消息生产者&apos;)
            -&amp;gt;addOption(&apos;debug&apos;, null, Option::VALUE_OPTIONAL, &apos;是否调试：1是 0否&apos;)
            -&amp;gt;addOption(&apos;message_ids&apos;, null, Option::VALUE_OPTIONAL, &apos;消息唯一ID列表&apos;)
            -&amp;gt;addArgument(&apos;task&apos;, null,  &apos;业务类型&apos;);
    }

    protected function execute(Input $input, Output $output)
    {
        $exitCode = 0;

        try {
            // 在执行前记录日志，第二个参数设置是否捕获输出
            $this-&amp;gt;beforeExecute($input, false); // 设置为 false 则不捕获输出

            // 业务逻辑
            $output-&amp;gt;writeln(&quot;command is start...&quot;);
            $task = $input-&amp;gt;getArgument(&apos;task&apos;);
            $this-&amp;gt;debug = $input-&amp;gt;getOption(&apos;debug&apos;) ?? &apos;0&apos;;
            $this-&amp;gt;message_ids = $input-&amp;gt;getOption(&apos;message_ids&apos;) ?? &apos;&apos;;

            if (method_exists($this, $task)) {
                $this-&amp;gt;$task($output);
            } else {
                $output-&amp;gt;writeln(&quot;Task &apos;$task&apos; not found.&quot;);
            }
            $output-&amp;gt;writeln(&apos;successfully!&apos;);
        } catch (\Throwable $e) {
            dump($e);
            $exitCode = $e-&amp;gt;getCode() ?: 1;
            $this-&amp;gt;handleException($e);
            if (Db::connect()-&amp;gt;getPdo()-&amp;gt;inTransaction()) {
                Db::rollback();
            }
        } finally {
            $errorMessage = $this-&amp;gt;lastError ? $this-&amp;gt;lastError[&apos;message&apos;] : &apos;&apos;;
            if ($errorMessage) {
                $output-&amp;gt;writeln($errorMessage);
            }
            $this-&amp;gt;afterExecute($output, $exitCode, $errorMessage);
        }

        return $exitCode;
    }

    /**
     * @command php think producer:dead_letter_message dlxMessageRequeue --message_ids=&quot;1,2&quot;
     * @taskname 失败消息重新入队生产者
     */
    protected function dlxMessageRequeue(): void
    {
        $service = DeadLetterMessageLogic::getInstance();
        // 处理参数
        $service-&amp;gt;message_ids = !empty($this-&amp;gt;message_ids) ? explode(&apos;,&apos;, $this-&amp;gt;message_ids) : [];
        $service-&amp;gt;debug = $this-&amp;gt;debug == 1;

        $service-&amp;gt;requeueDlxMessages();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;业务层代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\command\logic;

use app\common\enum\AmqpQueueEnum;
use app\common\enum\CacheEnum;
use app\repository\AmqpDeadLetterMessageRepository;
use core\Library\CacheHelper;
use core\Library\Rabbitmq;
use core\Trait\AmqpDlxTrait;
use core\Trait\SingletonTrait;
use think\facade\Cache;
use think\facade\Log;

class DeadLetterMessageLogic
{
    use SingletonTrait;
    use AmqpDlxTrait;

    // 唯一消息ID列表
    public array $message_ids = [];

    // 是否调试模式
    public bool $debug = false;

    /**
     * 重新入队死信消息
     * @return void
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function requeueDlxMessages(): void
    {
        try {
            if (empty($this-&amp;gt;message_ids)) {
                $this-&amp;gt;debugLog(&apos;参数为空&apos;, &apos;error&apos;);
                return;
            }

            $repository = AmqpDeadLetterMessageRepository::getInstance();
            $list_collect = $repository-&amp;gt;getModel()
                -&amp;gt;field([&apos;id&apos;, &apos;message_id&apos;, &apos;exchange&apos;, &apos;routing_key&apos;, &apos;queue_name&apos;, &apos;queue_config&apos;, &apos;exchange_type&apos;, &apos;message_content&apos;])
                -&amp;gt;whereIn(&apos;message_id&apos;, $this-&amp;gt;message_ids)
                -&amp;gt;select();
            if ($list_collect-&amp;gt;isEmpty()) {
                $this-&amp;gt;debugLog(&apos;没有符合条件的死信消息&apos;, &apos;error&apos;);
                return;
            }
            $list = $list_collect-&amp;gt;toArray();
            $this-&amp;gt;debugLog(&apos;开始处理死信消息:&apos; . count($list));

            $redis = Cache::store(&apos;redis&apos;)-&amp;gt;handler();

            foreach ($list as $item) {
                try {
                    $this-&amp;gt;debugLog(&quot;[死信消息重回队列]正在处理消息ID:{$item[&apos;message_id&apos;]},队列配置为：{$item[&apos;queue_config&apos;]}&quot; .
                        &quot;消息内容为：&quot; . json_encode($item[&apos;message_content&apos;], JSON_UNESCAPED_UNICODE));
                    $item_message_id = strval($item[&apos;message_id&apos;]);
                    if (!$this-&amp;gt;debug) {
                        // 删除防止重复消费键
                        $redis_key = CacheEnum::CHECK_QUEUE_REPEAT_CONSUMING_KEY . &apos;:&apos; . app(&apos;env&apos;)-&amp;gt;get(&apos;app_env&apos;) . &quot;:{$item_message_id}&quot;;
                        $redis-&amp;gt;del($redis_key);

                        // 重新入队列
                        $rabbitmq = new Rabbitmq();
                        $rabbitmq-&amp;gt;sendMessage($item[&apos;message_content&apos;], $item[&apos;queue_config&apos;], $item_message_id);
                    }

                    // 更新死信消息为处理中 因为重新入队了
                    $saveData = [
                        &apos;message_id&apos; =&amp;gt; $item_message_id,
                        &apos;queue_config&apos; =&amp;gt; $item[&apos;queue_config&apos;],
                        &apos;retry_count&apos; =&amp;gt; 0,
                        &apos;last_retry_time&apos; =&amp;gt; date(&apos;Y-m-d H:i:s&apos;),
                        &apos;status&apos; =&amp;gt; AmqpQueueEnum::STATUS_PADDING,
                        &apos;error_msg&apos; =&amp;gt; [&apos;error_msg&apos; =&amp;gt; &apos;&apos;]
                    ];
                    !$this-&amp;gt;debug &amp;amp;&amp;amp; $this-&amp;gt;saveDlxMessage($saveData, false);

                } catch (\Exception $e) {
                    $this-&amp;gt;debugLog(&quot;[死信消息重回队列异常]消息ID:{$item[&apos;message_id&apos;]},队列配置为：{$item[&apos;queue_config&apos;]}&quot; .
                        &quot;消息内容为：&quot; . json_encode($item[&apos;message_content&apos;], JSON_UNESCAPED_UNICODE) .
                        &quot;，错误原因:&quot; . var_export($e-&amp;gt;getMessage(), true),  &apos;error&apos;);
                }
            }
        } catch (\Exception $e) {
            $this-&amp;gt;debugLog(&quot;[死信消息重回队列致命错误]&quot; . var_export($e-&amp;gt;getMessage(), true), &apos;error&apos;);
        }

    }


    /**
     * 输出信息 并记录日志
     * @param string $msg
     * @return void
     */
    protected function debugLog(string $msg, $func = &apos;info&apos;): void
    {
        if (!$this-&amp;gt;debug &amp;amp;&amp;amp; $func != &apos;error&apos;) {
            return;
        }
        echo $msg . PHP_EOL;
        Log::$func($msg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果有某个失败消息 希望重新入队消费 则执行php think producer:dead_letter_message dlxMessageRequeue --message_ids=&quot;1,2&quot;即可重新入队。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;12. 配置命令行&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;配置好config/console.php即可运行以上命令&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// +----------------------------------------------------------------------
// | 控制台配置
// +----------------------------------------------------------------------
return [
    // 指令定义
    &apos;commands&apos; =&amp;gt; [
        //消费者
        &apos;consumer:test_consumer&apos; =&amp;gt; \app\command\definition\consumer\TestConsumerCommand::class,
        &apos;consumer:change_notify&apos; =&amp;gt; \app\command\definition\consumer\ChangeNotifyCommand::class,
        // 生产者
        &apos;producer:test_producer&apos; =&amp;gt; \app\command\definition\producer\TestProducerCommand::class,
        &apos;producer:dead_letter_message&apos; =&amp;gt; \app\command\definition\producer\DeadLetterMessageCommand::class,
    ],
];

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>🍭cloudcanal同步mysq到clickhouse实时方案</title><link>https://www.corgi37.top/posts/cloudcanal-tong-bu-mysq-dao-clickhouse-shi-shi-fang-an/</link><guid isPermaLink="true">https://www.corgi37.top/posts/cloudcanal-tong-bu-mysq-dao-clickhouse-shi-shi-fang-an/</guid><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;主要解决 由mysql同步到click house，源端(mysql)更新某条数据 在对端(clickhouse)中会转换成insert语句，出现两条重复主键的问题。使用自定义代码实现 将update语句转换成先删除对端(clickhouse)数据 再进行插入数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 创建同步任务&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 如果需要将更新改为删除后插入（使用自定义代码） 数据库名称需要保持一致&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zos.corgi37.top:15237/imgs/2025/08/20/68a56cecde532.png&quot; alt=&quot;cloudcanal_1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.上传自定义代码&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;需要使用到官方demo中的&lt;strong&gt;MysqlToClickHouseUpdateNoRepeat&lt;/strong&gt;类 需要参考&lt;strong&gt;MySqlToChNoUpdate&lt;/strong&gt;修改继承为&lt;strong&gt;CloudCanalProcessorV2&lt;/strong&gt;，代码也需要对应修改&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitee.com/clougence/cloudcanal-data-process/blob/master/data-transform/src/main/java/com/clougence/cloudcanal/dataprocess/datatransform/MysqlToClickHouseUpdateNoRepeat.java&quot;&gt;官方demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;拉下来后需要用到子工程data-transform&lt;/p&gt;
&lt;p&gt;修改plugin.properties文件内容为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loadClassName=com.clougence.cloudcanal.dataprocess.datatransform.MysqlToClickHouseUpdateNoRepeat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后打成jar包上传，创建任务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://oss.corgi37.top/corgi/cardof_blog/data-transform-1.0.0-SNAPSHOT(21).jar&quot;&gt;下载带日志版jar包&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://oss.corgi37.top/corgi/cardof_blog/data-transform-1.0.0-SNAPSHOT(19).jar&quot;&gt;下载不带日志纯净版jar包&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zos.corgi37.top:15237/imgs/2025/08/20/68a575e3da7d4.png&quot; alt=&quot;cloudcanal_2&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 查看日志是否报错&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;查看任务详情 查看日志&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zos.corgi37.top:15237/imgs/2025/08/20/68a577fdb8c7e.png&quot; alt=&quot;cloudcanal_3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zos.corgi37.top:15237/imgs/2025/08/20/68a578202869b.png&quot; alt=&quot;cloudcanal_4&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;修改源端某条数据 如果没有报错 且对端数据同步后不会变成两条数据 则代表成功&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🍥Hyperf3.1使用RabbitMQ</title><link>https://www.corgi37.top/posts/hyperf31-shi-yong-rabbitmq/</link><guid isPermaLink="true">https://www.corgi37.top/posts/hyperf31-shi-yong-rabbitmq/</guid><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;实现了消息消费失败入mysql数据库，提供了消息重新入队的command，有手动重试机制、防止消息重复消费逻辑。暂未实现延时队列&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 安装扩展包&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;composer require hyperf/amqp
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 生成配置文件&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;运行命令自动生成配置文件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;php bin/hyperf.php vendor:publish hyperf/amqp
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;修改下配置项 /config/autoload/amqp.php&lt;/p&gt;
&lt;p&gt;可在 &lt;code&gt;producer&lt;/code&gt; 或者 &lt;code&gt;consumer&lt;/code&gt; 的 &lt;code&gt;__construct&lt;/code&gt; 函数中，设置不同 &lt;code&gt;pool&lt;/code&gt;，例如下面的 &lt;code&gt;default&lt;/code&gt; 和 &lt;code&gt;crm&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
use Hyperf\Amqp\IO\IOFactory;

use function Hyperf\Support\env;

// 默认配置 如果需要项目隔离vhost 赋值新数组并更改vhost参数项
$default_config = [
    &apos;host&apos; =&amp;gt; env(&apos;AMQP_HOST&apos;, &apos;localhost&apos;),
    &apos;port&apos; =&amp;gt; (int) env(&apos;AMQP_PORT&apos;, 5672),
    &apos;user&apos; =&amp;gt; env(&apos;AMQP_USER&apos;, &apos;guest&apos;),
    &apos;password&apos; =&amp;gt; env(&apos;AMQP_PASSWORD&apos;, &apos;guest&apos;),
    &apos;vhost&apos; =&amp;gt; env(&apos;AMQP_VHOST&apos;, &apos;center&apos;),
    &apos;open_ssl&apos; =&amp;gt; false,
    &apos;concurrent&apos; =&amp;gt; [
        // 单个消费者进程内的并发消费上限
        &apos;limit&apos; =&amp;gt; 2,
    ],
    &apos;pool&apos; =&amp;gt; [
        // 连接池数量
        &apos;connections&apos; =&amp;gt; 2,
    ],
    &apos;io&apos; =&amp;gt; IOFactory::class,
    &apos;params&apos; =&amp;gt; [
        &apos;insist&apos; =&amp;gt; false,
        &apos;login_method&apos; =&amp;gt; &apos;AMQPLAIN&apos;,
        &apos;login_response&apos; =&amp;gt; null,
        &apos;locale&apos; =&amp;gt; &apos;en_US&apos;,
        &apos;connection_timeout&apos; =&amp;gt; 10,
        // 尽量保持是 heartbeat 数值的两倍
        &apos;read_write_timeout&apos; =&amp;gt; 60,
        &apos;context&apos; =&amp;gt; null,
        // 网络层保活 与心跳互补 兜底检测
        &apos;keepalive&apos; =&amp;gt; true,
        // 尽量保证每个消息的消费时间小于心跳时间
        &apos;heartbeat&apos; =&amp;gt; 30,
        &apos;channel_rpc_timeout&apos; =&amp;gt; 0.0,
        // 是否在对象析构时自动关闭连接
        &apos;close_on_destruct&apos; =&amp;gt; false,
        // 连接内可复用的空闲 Channel 数上限
        &apos;max_idle_channels&apos; =&amp;gt; 10,
        &apos;connection_name&apos; =&amp;gt; null,
    ],
];

// 如果需要隔离项目vhost
// 商家
$merchant_config = $default_config;
$merchant_config[&apos;vhost&apos;] = env(&apos;MERCHANT_AMQP_VHOST&apos;, &apos;merchant&apos;);

return [
    &apos;enable&apos; =&amp;gt; true,
    &apos;default&apos; =&amp;gt; $default_config,
    &apos;merchant&apos; =&amp;gt; $merchant_config,
];

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 配置.env文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# RabbitMQ
AMQP_HOST=127.0.0.1
AMQP_PORT=5672
AMQP_USER=guest
AMQP_PASSWORD=guest
AMQP_VHOST=center
MERCHANT_AMQP_VHOST=merchant
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 创建队列常量&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Common\Enum;
class AmqpEnum
{
    // AMQP重试次数
    const AMQP_RETRY_COUNT = 3;

    const STATUS_INIT = 0;
    const STATUS_PADDING = 1;
    const STATUS_SUCCESS = 2;
    const STATUS_FAILED = 3;
    const STATUS_DESC = [
        self::STATUS_INIT =&amp;gt; &apos;待处理&apos;,
        self::STATUS_PADDING =&amp;gt; &apos;处理中&apos;,
        self::STATUS_SUCCESS =&amp;gt; &apos;处理成功&apos;,
        self::STATUS_FAILED =&amp;gt; &apos;处理失败&apos;
    ];

    // 默认vhost配置项
    const DEFAULT_VHOST_CONFIG = &apos;default&apos;;

    // 商家vhost配置项
    const MERCHANT_VHOST_CONFIG = &apos;merchant&apos;;

    // 测试队列配置名称
    const DEMO_QUEUE_NAME = &apos;demo&apos;;
    // 商家端 测试队列配置名称
    const M_DEMO_QUEUE_NAME = &apos;m_demo&apos;;



}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.创建队列相关配置文件&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;创建配置文件&lt;code&gt;/config/autoload/amqp_queue.php&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare(strict_types=1);

use App\Amqp\Producer\Merchant\DyCallBackProducer;
use Hyperf\Amqp\Message\Type;
use function Hyperf\Support\env;
use App\Amqp\Producer\Demo\DemoProducer;
use App\Common\Enum\AmqpEnum;

$env = env(&apos;APP_ENV&apos;, &apos;dev&apos;);
return [
    // 默认配置
    AmqpEnum::DEFAULT_VHOST_CONFIG =&amp;gt; [
        &apos;vhost&apos; =&amp;gt; env(&apos;AMQP_VHOST&apos;, &apos;center&apos;),
        &apos;queues&apos; =&amp;gt; [
            // 命名规则 队列名称.项目名称（默认center）.环境
            AmqpEnum::DEMO_QUEUE_NAME =&amp;gt; [
                &apos;exchange&apos; =&amp;gt; &quot;test.demo.center.{$env}&quot;,
                &apos;exchange_type&apos; =&amp;gt; Type::DIRECT-&amp;gt;value, // 交换机类型 direct fanout topic headers
                &apos;queue&apos; =&amp;gt; &quot;test.demo.queue.center.{$env}&quot;, // 队列名
                &apos;routing_key&apos; =&amp;gt; &quot;test.demo.key.center.{$env}&quot;, // 路由键
                &apos;producer&apos; =&amp;gt; DemoProducer::class, // 生产者
            ]
        ]
    ],
    // 商家配置
    AmqpEnum::MERCHANT_VHOST_CONFIG =&amp;gt; [
        &apos;vhost&apos; =&amp;gt; env(&apos;MERCHANT_AMQP_VHOST&apos;, &apos;merchant&apos;),
        &apos;queues&apos; =&amp;gt; [
            // 命名规则 队列名称.项目名称（merchant）.环境
            AmqpEnum::M_DEMO_QUEUE_NAME =&amp;gt; [
                &apos;exchange&apos; =&amp;gt; &quot;test.m_demo.merchant.{$env}&quot;,
                &apos;exchange_type&apos; =&amp;gt; Type::FANOUT-&amp;gt;value, // 交换机类型 direct fanout topic headers
                &apos;queue&apos; =&amp;gt; &quot;test.m_demo.queue.merchant.{$env}&quot;, // 队列名
                &apos;routing_key&apos; =&amp;gt; &quot;test.m_demo.key.merchant.{$env}&quot;, // 路由键
              	&apos;producer&apos; =&amp;gt; DemoProducer::class, // 生产者
            ]
        ]
    ],
];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.创建缓存键常量&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Common\Enum;

class CacheEnum
{
    // 防止队列重复消费键
    const CHECK_QUEUE_REPEAT_CONSUMING_KEY = &apos;check_queue_repeat_consuming&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. 创建失败队列消息表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `ufo_m_amqp_dead_letter_message` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `message_id` varchar(255) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;消息ID&apos;,
  `exchange` varchar(255) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;原始交换机&apos;,
  `routing_key` varchar(255) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;原始路由键&apos;,
  `queue_name` varchar(255) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;队列名称&apos;,
  `queue_config` varchar(255) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;队列配置名（区分业务）&apos;,
  `exchange_type` varchar(255) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;交换机类型：direct fanout topic headers&apos;,
  `message_content` json  COMMENT &apos;消息内容&apos;,
  `retry_count` int(11) unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;重试次数&apos;,
  `last_retry_time` datetime DEFAULT NULL COMMENT &apos;最后重试时间&apos;,
  `status` tinyint(4) unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;状态(0-待处理,1-处理中,2-处理成功,3-处理失败)&apos;,
  `error_msg` json DEFAULT NULL COMMENT &apos;错误信息&apos;,
  `created_at` datetime DEFAULT NULL COMMENT &apos;创建时间&apos;,
  `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT &apos;更新时间&apos;,
  `deleted_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idxes_queue_config_status` (`queue_config`,`status`),
  KEY `idx_create_time` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=&apos;Amqp失败队列消息表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. 创建AmqpMessageTrait特征&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 用到了snowflake包（IdGeneratorInterface） 生成唯一消息ID&lt;/p&gt;
&lt;p&gt;运行&lt;code&gt;composer require hyperf/snowflake&lt;/code&gt;安装扩展&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Common\Trait;

use App\Common\Enum\AmqpEnum;
use App\Common\Enum\CacheEnum;
use App\Module\Merchant\Repository\AmqpDeadLetterMessageRepository as MerchantAmqpDeadLetterMessageRepository;
use Hyperf\Amqp\Constants;
use Hyperf\Amqp\Result;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Hyperf\Snowflake\IdGeneratorInterface;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
use function Hyperf\Config\config;

trait AmqpMessageTrait
{
    #[Inject]
    public Redis $redis;

    #[Inject]
    public MerchantAmqpDeadLetterMessageRepository $merchantAmqpDeadLetterMessageRepository;

    #[Inject]
    public IdGeneratorInterface $idGenerator;

    /**
     * 生成队列配置
     * @param string $pool_name 队列池名称
     * @param string $queue_name 队列配置名称
     * @param string $message_id 唯一消息ID
     * @return array
     */
    public function getConfig(string $pool_name, string $queue_name, string $message_id = &apos;&apos;): array
    {
        $poolName = $pool_name;

        // 队列配置
        $queue_config = config(&quot;amqp_queue.{$pool_name}.queues.&quot; . $queue_name);
        $exchange = $queue_config[&apos;exchange&apos;];
        $type = $queue_config[&apos;exchange_type&apos;];
        $routingKey = $queue_config[&apos;routing_key&apos;];
        $queue = $queue_config[&apos;queue&apos;];
        $properties = [
            &apos;content_type&apos; =&amp;gt; &apos;text/plain&apos;,
            &apos;delivery_mode&apos; =&amp;gt; Constants::DELIVERY_MODE_PERSISTENT,
            &apos;timestamp&apos; =&amp;gt; time(),
            &apos;application_headers&apos;=&amp;gt; new AMQPTable([
                &apos;retry&apos; =&amp;gt; &apos;0&apos;, // 重试次数
                &apos;message_id&apos; =&amp;gt; strval($message_id ?: $this-&amp;gt;idGenerator-&amp;gt;generate()), // 消息唯一ID
                &apos;queue_config&apos; =&amp;gt; $queue_name,
                &apos;exchange_type&apos; =&amp;gt; (string) $type,
                &apos;queue_name&apos; =&amp;gt; $queue_config[&apos;queue&apos;],
                &apos;vhost_config&apos; =&amp;gt; $pool_name
            ]),
        ];

        return compact(&apos;poolName&apos;, &apos;exchange&apos;, &apos;type&apos;, &apos;routingKey&apos;, &apos;properties&apos;, &apos;queue&apos;);
    }


    /**
     * 检查消息是否重复消费
     * @param AMQPMessage $msg
     * @return bool
     * @throws \RedisException
     */
    public function checkMessageRepeat(AMQPMessage $msg): bool
    {
        $msgData = $this-&amp;gt;messageHandle($msg, &apos;&apos;, false);
        $message_id = $msgData[&apos;message_id&apos;];
        $vhost_config = $msgData[&apos;vhost_config&apos;];
        // 重试的话不检查重复消费
        if (empty($message_id) || ($msgData[&apos;retry_count&apos;] ?? 0) &amp;gt; 0) {
            return true;
        }
        $redis_key = CacheEnum::CHECK_QUEUE_REPEAT_CONSUMING_KEY . &quot;:{$vhost_config}:&quot; . config(&apos;app_env&apos;) . &quot;:{$message_id}&quot;;

        // 一天内不能重复消费
        return $this-&amp;gt;redis-&amp;gt;setnx($redis_key, $message_id) &amp;amp;&amp;amp; $this-&amp;gt;redis-&amp;gt;expire($redis_key, 24 * 3600);
    }


    /**
     * 获取队列消息详细信息
     * @param AMQPMessage $msg
     * @param string $error_msg
     * @param bool $needGenerateId 不存在message_id时是否生成
     * @return array
     */
    public function messageHandle(AMQPMessage $msg, string $error_msg, bool $needGenerateId = true): array
    {
        $exchange = $msg-&amp;gt;getExchange();
        $routing_key = $msg-&amp;gt;getRoutingKey();
        $message_content = $msg-&amp;gt;getBody();
        //headersObject 是一个AMQPTable对象
        $headersObject = $msg-&amp;gt;get_properties()[&apos;application_headers&apos;];
        //调用getNativeData()得到一个数组
        $headersArray  = $headersObject-&amp;gt;getNativeData();

        if ($needGenerateId &amp;amp;&amp;amp; !isset($headersArray[&apos;message_id&apos;])) {
            $headersArray[&apos;retry&apos;] = 0;
            $headersArray[&apos;message_id&apos;] = strval($this-&amp;gt;idGenerator-&amp;gt;generate());
        }

        $message_id = $headersArray[&apos;message_id&apos;];
        $queue_name = $headersArray[&apos;queue_name&apos;] ?? &apos;&apos;;
        $queue_config = $headersArray[&apos;queue_config&apos;] ?? &apos;&apos;;
        $exchange_type = $headersArray[&apos;exchange_type&apos;] ?? &apos;&apos;;
        $vhost_config = $headersArray[&apos;vhost_config&apos;] ?? &apos;&apos;;
        $retry_count = ($headersArray[&apos;retry&apos;] ?? 0);
        $last_retry_time = date(&apos;Y-m-d H:i:s&apos;);
        $error_msg = compact(&apos;error_msg&apos;);

        return compact(&apos;exchange&apos;, &apos;routing_key&apos;, &apos;message_id&apos;, &apos;queue_name&apos;, &apos;error_msg&apos;, &apos;vhost_config&apos;,
            &apos;queue_config&apos;, &apos;exchange_type&apos;, &apos;retry_count&apos;, &apos;headersArray&apos;, &apos;message_content&apos;, &apos;last_retry_time&apos;);
    }

    /**
     * 丢弃消息
     * @param AMQPMessage $msg
     * @param string $error_msg 错误信息
     * @return Result
     */
    public function rejectHandle(AMQPMessage $msg, string $error_msg): Result
    {
        $msgData = $this-&amp;gt;messageHandle($msg, $error_msg);
        $msgData[&apos;status&apos;] = AmqpEnum::STATUS_FAILED;

        // 记录失败日志
        $this-&amp;gt;saveDlxMessage($msgData);
        return Result::DROP;
    }

    /**
     * 确认应答
     * @param AMQPMessage $msg
     * @return Result
     */
    public function ackHandle(AMQPMessage $msg): Result
    {
        $msgData = $this-&amp;gt;messageHandle($msg, &apos;&apos;);
        $msgData[&apos;status&apos;] = AmqpEnum::STATUS_SUCCESS;

        // 更新失败日志
        $this-&amp;gt;saveDlxMessage($msgData, false);
        return Result::ACK;
    }

    /**
     * 手动重试
     * @param AMQPMessage $msg
     * @param string $error_msg 错误信息
     * @param int $retry_num 重试次数
     * @return Result
     */
    public function retryHandle(AMQPMessage $msg, string $error_msg, int $retry_num = AmqpEnum::AMQP_RETRY_COUNT): Result
    {
        $msgData = $this-&amp;gt;messageHandle($msg, $error_msg);
        $exchange      = $msgData[&apos;exchange&apos;];
        $routing_key   = $msgData[&apos;routing_key&apos;];
        $channel       = $msg-&amp;gt;getChannel();
        $body          = $msgData[&apos;message_content&apos;];
        $headersArray  = $msgData[&apos;headersArray&apos;];


        if ($headersArray[&apos;retry&apos;] &amp;lt; $retry_num) {
            $headersArray[&apos;retry&apos;]++;
            // 重新入队列
            $channel-&amp;gt;basic_publish(
                new AMQPMessage($body, [
                    &apos;content_type&apos; =&amp;gt; &apos;text/plain&apos;,
                    &apos;delivery_mode&apos; =&amp;gt; AMQPMessage::DELIVERY_MODE_PERSISTENT,
                    &apos;timestamp&apos; =&amp;gt; time(),
                    &apos;application_headers&apos;=&amp;gt; new AMQPTable($headersArray)
                ]),
                $exchange,
                $routing_key
            );
            // 因为重新入队了 所以这条消息直接确认
            $result = Result::ACK;
            $msgData[&apos;status&apos;] = AmqpEnum::STATUS_PADDING;
        } else {
            // 丢弃
            $result = Result::DROP;
            $msgData[&apos;status&apos;] = AmqpEnum::STATUS_FAILED;
        }
        // 记录失败日志
        $this-&amp;gt;saveDlxMessage($msgData);
        return $result;
    }

    /**
     * 保存失败队列消息
     * @param array $saveData
     * @param bool $needInsert 记录不存在是否新增
     * @return bool
     */
    public function saveDlxMessage(array $saveData, bool $needInsert = true): bool
    {
        if (empty($saveData) || empty($saveData[&apos;message_id&apos;])) {
            return false;
        }
        $saveWhere = [
            &apos;message_id&apos; =&amp;gt; $saveData[&apos;message_id&apos;],
            &apos;queue_config&apos; =&amp;gt; $saveData[&apos;queue_config&apos;],
        ];

        $saveData[&apos;update_time&apos;] = date(&apos;Y-m-d H:i:s&apos;);
        $saveData[&apos;error_msg&apos;] = $saveData[&apos;error_msg&apos;] ?? [];


        $saveData[&apos;message_content&apos;] = empty($saveData[&apos;message_content&apos;]) ? null : (is_string($saveData[&apos;message_content&apos;])
            ? json_decode($saveData[&apos;message_content&apos;], true) : $saveData[&apos;message_content&apos;]);

        // 根据不同项目 写入不同表
        $vhost_config = $saveData[&apos;vhost_config&apos;] ?? AmqpEnum::DEFAULT_VHOST_CONFIG;
        $repository = $this-&amp;gt;getDlxMessageRepository($vhost_config);

        return $repository-&amp;gt;insertOrUpdate($saveWhere, $saveData, $needInsert);
    }


    /**
     * 根据不同vhost配置 返回不同项目的失败消息表
     * @param string $vhost_config
     */
    public function getDlxMessageRepository(string $vhost_config)
    {
        switch ($vhost_config) {
            case AmqpEnum::MERCHANT_VHOST_CONFIG:
                // 商家端
                $repository = $this-&amp;gt;merchantAmqpDeadLetterMessageRepository;
                break;
            default:
                $repository = $this-&amp;gt;merchantAmqpDeadLetterMessageRepository;
        }
        return $repository;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;$repository-&amp;gt;insertOrUpdate()方法类似这样&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 插入或更新
 * @param array $where
 * @param array $data
 * @param bool $needInsert 记录不存在是否新增
 * @return bool
 */
public function insertOrUpdate(array $where, array $data, bool $needInsert = true): bool
{
    // 如果传入了 $where 条件，尝试查找记录
    if (!empty($where)) {
        $record = AmqpDeadLetterMessage::query()-&amp;gt;where($where)-&amp;gt;first();
        if ($record) {
            // 记录存在，执行更新
            return $record-&amp;gt;fill($data)-&amp;gt;save();
        }
    }

    // 记录不存在，执行新增
    if ($needInsert) {
        return (new AmqpDeadLetterMessage())-&amp;gt;fill($data)-&amp;gt;save();
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;模型需要定义以下属性&lt;/p&gt;
&lt;p&gt;主要是message_content、error_msg两个json类型的字段 交给hyperf处理数组&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;protected array $fillable = [&apos;id&apos;, &apos;message_id&apos;, &apos;exchange&apos;, &apos;routing_key&apos;, &apos;queue_name&apos;, &apos;queue_config&apos;, &apos;exchange_type&apos;, &apos;message_content&apos;, &apos;retry_count&apos;, &apos;last_retry_time&apos;, &apos;status&apos;, &apos;error_msg&apos;, &apos;created_at&apos;, &apos;updated_at&apos;, &apos;deleted_at&apos;];


protected array $casts = [&apos;id&apos; =&amp;gt; &apos;integer&apos;, &apos;retry_count&apos; =&amp;gt; &apos;integer&apos;, &apos;status&apos; =&amp;gt; &apos;integer&apos;, &apos;created_at&apos; =&amp;gt; &apos;datetime&apos;, &apos;updated_at&apos; =&amp;gt; &apos;datetime&apos;, &apos;message_content&apos; =&amp;gt; &apos;array&apos;, &apos;error_msg&apos; =&amp;gt; &apos;array&apos;];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9. 创建生产者&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;运行php bin/hyperf.php gen:amqp-producer DemoProducer 下载官方demo&lt;/p&gt;
&lt;p&gt;修改代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

declare(strict_types=1);

namespace App\Amqp\Producer\Demo;

use App\Common\Enum\AmqpEnum;
use App\Common\Trait\AmqpMessageTrait;
use Hyperf\Amqp\Message\ProducerMessage;

/**
 * 测试生产者
 * Class DemoProducer
 * @package App\Amqp\Producer\Demo
 */
class DemoProducer extends ProducerMessage
{
    use AmqpMessageTrait;
    public function __construct($data, string $message_id = &apos;&apos;)
    {
        $this-&amp;gt;setConfig($message_id);
        // 需要发送的数据
        $this-&amp;gt;payload = $data;
    }

    /**
     * 设置队列配置
     * @param string $message_id
     * @return void
     */
    public function setConfig(string $message_id = &apos;&apos;): void
    {
        $params = $this-&amp;gt;getConfig(AmqpEnum::DEFAULT_VHOST_CONFIG, AmqpEnum::DEMO_QUEUE_NAME, $message_id);
        fillProperty($params, $this);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中用到了填充属性的方法fillProperty&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;if (!function_exists(&apos;fillProperty&apos;)) {
    /**
     * 使用反射填充属性
     * @param array $params
     * @param object $instance
     */
    function fillProperty(array $params, object $instance): void
    {
        $ref = new ReflectionObject($instance);
        foreach ($params as $key =&amp;gt; $value) {
            if ($ref-&amp;gt;hasProperty($key)) {
                $prop = $ref-&amp;gt;getProperty($key);
                $prop-&amp;gt;setAccessible(true); // 允许访问 protected/private
                $prop-&amp;gt;setValue($instance, $value);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10.创建一个控制器测试生产者&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Controller;

use App\Amqp\Producer\Demo\DemoProducer;
use Hyperf\Amqp\Producer;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\PostMapping;

#[Controller(prefix: &apos;demo&apos;)]
class DemoController extends AbstractController
{
    #[Inject]
    protected Producer $producer;

    /**
     * demo 生产者
     * @return array
     */
    #[PostMapping(path: &apos;demoProduce&apos;)]
    public function demoProduce()
    {
        $res = $this-&amp;gt;producer-&amp;gt;produce(new DemoProducer([&apos;tenant_id&apos; =&amp;gt; 1, &apos;product_list&apos; =&amp;gt; [1,2]], &apos;test_message_id&apos;));

        return [
            &apos;message&apos; =&amp;gt; $res ? &quot;发送成功&quot; : &apos;发送失败&apos;,
        ];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;post方式调用127.0.0.1:9501/demo/demoProduce即可在rabbitmq后台看到exchange生成了，但队列没有生成（正常现象，设计理念就是生产者只负责消息的封装与发送，不会自动帮你创建队列，队列的声明和绑定动作需要在消费者里实现），消费者启动后队列就生成了（消费者跟随项目启动 无需手动启动）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;11.创建消费者&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;运行&lt;code&gt;php bin/hyperf.php gen:amqp-consumer DemoConsumer&lt;/code&gt;下载官方消费者demo&lt;/p&gt;
&lt;p&gt;修改代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

declare(strict_types=1);

namespace App\Amqp\Consumer\Demo;

use App\Amqp\Consumer\BasicConsumer;
use App\Common\Enum\AmqpEnum;
use App\Exception\BusinessException;
use Hyperf\Amqp\Annotation\Consumer;
use Hyperf\Amqp\Result;
use PhpAmqpLib\Exception\AMQPChannelClosedException;
use PhpAmqpLib\Exception\AMQPConnectionBlockedException;
use PhpAmqpLib\Exception\AMQPConnectionClosedException;
use PhpAmqpLib\Exception\AMQPIOException;
use PhpAmqpLib\Message\AMQPMessage;

#[Consumer(name: &quot;DemoConsumer&quot;, nums: 1)]
class DemoConsumer extends BasicConsumer
{
    public function __construct()
    {
        $this-&amp;gt;setConfig();
    }

    /**
     * 设置队列配置
     * @return void
     */
    public function setConfig(): void
    {
        $params = $this-&amp;gt;getConfig(AmqpEnum::DEFAULT_VHOST_CONFIG, AmqpEnum::DEMO_QUEUE_NAME);
        fillProperty($params, $this);
    }

    public function consumeMessage($data, AMQPMessage $message): Result
    {
        $json_data = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        try {

            $this-&amp;gt;logger-&amp;gt;info(&apos;接收到消息：&apos; . $json_data);

            // 检查消息是否重复消费
            if (!$this-&amp;gt;checkMessageRepeat($message)) {
                throw new BusinessException(&quot;消息重复消费,本次跳过&quot;);
            }

            // todo 业务处理


            // 确认应答
            return $this-&amp;gt;ackHandle($message);
        }  catch (BusinessException|AMQPIOException|AMQPConnectionClosedException|AMQPConnectionBlockedException|AMQPChannelClosedException|\Exception $e) {
            if ($e instanceof BusinessException) {
                $this-&amp;gt;logger-&amp;gt;error(&quot;[测试消费者]发生业务性异常 消息丢弃：&quot; . $json_data . &quot;，错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true));
                $error_msg = &quot;[业务异常]错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true);
                // 丢弃消息
                return $this-&amp;gt;rejectHandle($message, $error_msg);
            } else {
                $this-&amp;gt;logger-&amp;gt;error(&quot;[测试消费者]发生致命异常 消息重新入队：&quot; . $json_data . &quot;，错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true));
                $error_msg = &quot;[致命异常]错误原因：&quot; . var_export($e-&amp;gt;getMessage(), true);
                // 手动重试机制
                return $this-&amp;gt;retryHandle($message, $error_msg);
            }
        }

    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中用到BusinessException异常类 主要是为了区分 消息是丢弃还是重试&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
namespace App\Exception;

use Throwable;

class BusinessException extends \RuntimeException
{
    protected $code = 400;

    public function __construct($message = &quot;&quot;, $code = 0, Throwable $previous = null)
    {
        $this-&amp;gt;code = $code ?: $this-&amp;gt;code;
        parent::__construct($message, $this-&amp;gt;code, $previous);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里还修改了继承类为BasicConsumer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Amqp\Consumer;

use App\Common\Trait\AmqpMessageTrait;
use Hyperf\Amqp\Message\ConsumerMessage;
use Hyperf\Di\Annotation\Inject;
use Psr\Log\LoggerInterface;

class BasicConsumer extends ConsumerMessage
{
    use AmqpMessageTrait;

    #[Inject]
    protected LoggerInterface $logger;

    /**
     * 判断是否允许自启动
     * @return bool
     */
    public function isEnable(): bool
    {
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果需要禁止所有消费者启动 修改isEnable方法即可，如果需要单个消费者启动 重写isEnable方法即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;12.失败消息重新入队脚本&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;创建一个命令行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace App\Command;

use App\Command\Service\DeadLetterMessageService;
use App\Common\Enum\AmqpEnum;
use Hyperf\Command\Annotation\Command;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Di\Annotation\Inject;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

/**
 * 失败消息重新入队
 * Class DeadLetterMessageCommand
 * @package App\Command
 */
#[Command(
    name: &quot;dlxMessage:requeue&quot;,
    arguments:[
        [&apos;vhost_config&apos;, InputArgument::REQUIRED, &apos;vhost配置名称：default|merchant&apos;],
        [&apos;message_ids&apos;, InputArgument::REQUIRED, &apos;消息ID，多个用英文逗号隔开&apos;]
    ],
    options:[
        [&apos;debug&apos;, null, InputOption::VALUE_OPTIONAL, &apos;是否调试模式&apos;, &apos;0&apos;]
    ],
    description:&quot;失败消息重新入队&quot;)
]
class DeadLetterMessageCommand extends HyperfCommand
{
    #[Inject]
    protected LoggerInterface $logger;

    #[Inject]
    protected DeadLetterMessageService $service;

    public function handle()
    {
        try {
            $start = microtime(true);
            $vhost_config = $this-&amp;gt;input-&amp;gt;getArgument(&apos;vhost_config&apos;) ?: AmqpEnum::DEFAULT_VHOST_CONFIG;
            $message_ids = $this-&amp;gt;input-&amp;gt;getArgument(&apos;message_ids&apos;);
            $debug = $this-&amp;gt;input-&amp;gt;getOption(&apos;debug&apos;);

            $message_ids = !empty($message_ids) ? explode(&apos;,&apos;, $message_ids) : [];

            $this-&amp;gt;service-&amp;gt;requeueDlxMessages($vhost_config, $message_ids, $debug == 1);

            $duration = round(microtime(true) - $start, 2);
            $this-&amp;gt;info(&quot;失败消息重新入队操作完成，耗时：{$duration}秒&quot;);
        } catch (\Throwable $e) {
            // 简单记录错误
            $this-&amp;gt;logger-&amp;gt;error(&apos;失败消息重新入队系统错误&apos;, [
                &apos;error&apos; =&amp;gt; $e-&amp;gt;getMessage(),
                &apos;file&apos; =&amp;gt; $e-&amp;gt;getFile(),
                &apos;line&apos; =&amp;gt; $e-&amp;gt;getLine()
            ]);
            $this-&amp;gt;info(&apos;失败消息重新入队系统错误！&apos; . var_export($e-&amp;gt;getMessage(), true));
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;创建业务类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\Command\Service;

use App\Common\Enum\AmqpEnum;
use App\Common\Enum\CacheEnum;
use App\Common\Trait\AmqpMessageTrait;
use App\Exception\BusinessException;
use Hyperf\Amqp\Producer;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Psr\Log\LoggerInterface;
use function Hyperf\Config\config;

class DeadLetterMessageService
{
    use AmqpMessageTrait;

    #[Inject]
    protected LoggerInterface $logger;

    #[Inject]
    protected Producer $producer;

    #[Inject]
    public Redis $redis;

    /**
     * 重新入队死信消息
     * @param string $vhost_config vhost配置名称
     * @param array $message_ids 消息ID
     * @param bool $debug 是否调试模式
     * @return void
     */
    public function requeueDlxMessages(string $vhost_config, array $message_ids, bool $debug): void
    {
        if (empty($message_ids) || empty($vhost_config)) {
            $this-&amp;gt;logger-&amp;gt;error(&apos;参数非法&apos;);
            throw new BusinessException(&apos;参数非法&apos;);
        }

        $repository = $this-&amp;gt;getDlxMessageRepository($vhost_config);
        $list = $repository-&amp;gt;getListByMessageIds($message_ids);
        $container = ApplicationContext::getContainer();

        if (empty($list)) {
            $this-&amp;gt;logger-&amp;gt;error(&apos;没有符合条件的死信消息&apos;);
            throw new BusinessException(&apos;没有符合条件的死信消息&apos;);
        }

        foreach ($list as $item) {
            try {
                $item_message_id = strval($item[&apos;message_id&apos;]);
                if (!$debug) {
                    // 删除防止重复消费键

                    $redis_key = CacheEnum::CHECK_QUEUE_REPEAT_CONSUMING_KEY . &quot;:{$vhost_config}:&quot; . config(&apos;app_env&apos;) . &quot;:{$item_message_id}&quot;;
                    $this-&amp;gt;redis-&amp;gt;del($redis_key);


                    // 获取队列配置
                    $queue_config = config(&quot;amqp_queue.{$vhost_config}.queues.&quot; . $item[&apos;queue_config&apos;]);

                    $producer = $container-&amp;gt;make($queue_config[&apos;producer&apos;], [
                        $item[&apos;message_content&apos;] ?: [],
                        $item_message_id
                    ]);

                    // 重新入队列
                    $this-&amp;gt;producer-&amp;gt;produce($producer);
                }

                // 更新死信消息为处理中 因为重新入队了
                $saveData = [
                    &apos;message_id&apos; =&amp;gt; $item_message_id,
                    &apos;queue_config&apos; =&amp;gt; $item[&apos;queue_config&apos;],
                    &apos;retry_count&apos; =&amp;gt; 0,
                    &apos;last_retry_time&apos; =&amp;gt; date(&apos;Y-m-d H:i:s&apos;),
                    &apos;status&apos; =&amp;gt; AmqpEnum::STATUS_PADDING,
                    &apos;error_msg&apos; =&amp;gt; [&apos;error_msg&apos; =&amp;gt; &apos;&apos;]
                ];
                !$debug &amp;amp;&amp;amp; $this-&amp;gt;saveDlxMessage($saveData, false);

            } catch (\Exception $e) {
                $this-&amp;gt;logger-&amp;gt;error(&quot;[死信消息重回队列异常]消息ID:{$item[&apos;message_id&apos;]},队列配置为：{$item[&apos;queue_config&apos;]}&quot; .
                    &quot;消息内容为：&quot; . json_encode($item[&apos;message_content&apos;], JSON_UNESCAPED_UNICODE) .
                    &quot;，错误原因:&quot; . var_export($e-&amp;gt;getMessage(), true));
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中用到了获取信息列表的方法 类似这样&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据消息ID获取列表
 * @param array $message_ids
 * @return array
 */
public function getListByMessageIds(array $message_ids): array
{
    $list = AmqpDeadLetterMessage::query()-&amp;gt;whereIn(&apos;message_id&apos;, $message_ids)-&amp;gt;get();
    if ($list-&amp;gt;isEmpty()) {
        return [];
    }
    return $list-&amp;gt;toArray();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;运行php bin/hyperf.php dlxMessage:requeue default &quot;1,2,3&quot;就可以将message_id为1、2、3的记录重新入队了。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🍛ThinkPHP8中使用redis缓存</title><link>https://www.corgi37.top/posts/thinkphp8-zhong-shi-yong-redis-huan-cun/</link><guid isPermaLink="true">https://www.corgi37.top/posts/thinkphp8-zhong-shi-yong-redis-huan-cun/</guid><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;实现了使用redis缓存自增版本、指定缓存版本号&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 在.env中添加以下配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Redis
REDIS_HOST = 127.0.0.1
REDIS_PORT = 6379
REDIS_PASS = password
REDIS_CACHE_SELECT = 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 修改缓存配置文件&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;config/cache.php&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------

return [
    // 默认缓存驱动
    &apos;default&apos; =&amp;gt; &apos;redis&apos;,

    // 缓存连接方式配置
    &apos;stores&apos;  =&amp;gt; [
        &apos;file&apos; =&amp;gt; [
            // 驱动方式
            &apos;type&apos;       =&amp;gt; &apos;File&apos;,
            // 缓存保存目录
            &apos;path&apos;       =&amp;gt; &apos;&apos;,
            // 缓存前缀
            &apos;prefix&apos;     =&amp;gt; &apos;&apos;,
            // 缓存有效期 0表示永久缓存
            &apos;expire&apos;     =&amp;gt; 0,
            // 缓存标签前缀
            &apos;tag_prefix&apos; =&amp;gt; &apos;tag:&apos;,
            // 序列化机制 例如 [&apos;serialize&apos;, &apos;unserialize&apos;]
            &apos;serialize&apos;  =&amp;gt; [],
        ],
        // 更多的缓存连接
        &apos;redis&apos; =&amp;gt; [
            &apos;type&apos; =&amp;gt; &apos;redis&apos;,
            // Redis 主机
            &apos;host&apos; =&amp;gt; env(&apos;REDIS_HOST&apos;, &apos;127.0.0.1&apos;),
            // Redis 端口
            &apos;port&apos; =&amp;gt; env(&apos;REDIS_PORT&apos;, 6379),
            // Redis 密码
            &apos;password&apos; =&amp;gt; env(&apos;REDIS_PASS&apos;, &apos;&apos;),
            // 库
            &apos;select&apos; =&amp;gt; env(&apos;REDIS_CACHE_SELECT&apos;, 2),
            // 缓存前缀
            &apos;prefix&apos; =&amp;gt; &apos;&apos;,
            // 缓存有效期 0表示永久缓存
            &apos;expire&apos; =&amp;gt; 0,
//            // 缓存标签前缀
//            &apos;tag_prefix&apos; =&amp;gt; &apos;tag:&apos;,
//            // 是否使用连接池
//            &apos;use_pool&apos; =&amp;gt; true,
//            // 连接池的连接标识
//            &apos;pool&apos; =&amp;gt; &apos;default&apos;,
        ],
    ],
];

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.缓存相关常量配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare (strict_types=1);

namespace app\common\enum;

class CacheEnum
{
    // 版本号前缀
    const CACHE_VERSION_PREFIX = &apos;cache_version_v1001&apos;;
    // 版本号初始值
    const DEFAULT_VERSION = 1001;
    // 缓存键前缀
    const CACHE_PREFIX = &apos;data_cache&apos;;

    /** 各种业务场景的缓存键 */
    // 生成链接缓存键
    const PRODUCT_DETAIL_GET_LINK_CACHE_KEY = &apos;product_detail_get_link&apos;;
    // 类目列表缓存键
    const PRODUCT_CATEGORY_LIST_CACHE_KEY = &apos;product_category_list&apos;;

    // 防止队列重复消费键
    const CHECK_QUEUE_REPEAT_CONSUMING_KEY = &apos;check_queue_repeat_consuming&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.创建缓存帮助类&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;提供了两种方法设置缓存 setCacheData与remember方法 使用前都需先调用setCacheConfig方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
declare(strict_types=1);

namespace core\Library;

use app\common\enum\CacheEnum;
use think\facade\Cache;

/**
 * 缓存类
 * 注意事项 需先调用setCacheConfig方法
 * Class CacheHelper
 * @package core\Library
 */
class CacheHelper
{
    // 初始版本号
    protected int $version = CacheEnum::DEFAULT_VERSION;

    // 是否自定义缓存键
    protected bool $is_set_version = false;

    // 缓存时间
    protected int|null $ttl = null;

    // 缓存键
    protected string $cache_key = &apos;default&apos;;

    // 未拼接前的缓存键
    protected string $cache_key_ori = &apos;default&apos;;


    // 版本号键
    protected string $version_key = &apos;default&apos;;

    /**
     * 使用闭包缓存
     * @param \Closure $callback
     * @return mixed
     */
    public function remember(\Closure $callback): mixed
    {
        // 获取缓存数据
        $cache_data = $this-&amp;gt;getCacheData();
        if ($cache_data !== null) {
            return $cache_data;
        }

        // 缓存不存在，调用闭包生成数据
        $data = $callback();
        // 设置缓存与版本号
        $this-&amp;gt;setCacheData($data);

        return $data;
    }


    /**
     * 设置缓存数据
     * @param mixed $data
     * @return bool
     */
    public function setCacheData(mixed $data): bool
    {
        $data_res = Cache::set($this-&amp;gt;getCacheKey(), $data, $this-&amp;gt;getTtl());
        $version_res = Cache::set($this-&amp;gt;getVersionKey(), $this-&amp;gt;getVersion());
        if ($data_res &amp;amp;&amp;amp; $version_res) {
            return true;
        }
        return false;
    }

    /**
     * 获取缓存数据
     * @return mixed
     */
    public function getCacheData(): mixed
    {
        if (Cache::has($this-&amp;gt;getCacheKey())) {
            return Cache::get($this-&amp;gt;getCacheKey());
        }
        return null;
    }


    /**
     * 获取当前缓存版本号
     * @return int
     */
    public function getVersion(): int
    {
        if (!$this-&amp;gt;is_set_version &amp;amp;&amp;amp; Cache::has($this-&amp;gt;version_key)) {
            // 未手动设置过缓存 缓存存在，直接返回
            $this-&amp;gt;version = intval(Cache::get($this-&amp;gt;version_key));
        }
        return $this-&amp;gt;version;
    }


    /**
     * 设置缓存配置
     * @param string $cache_key 缓存键名
     * @param int|null $ttl 缓存有效期 null为永久有效
     * @param bool $inc_version 是否递增版本号
     * @param int $version 是否自定义版本号 不建议与递增版本号同时使用 若同时使用 则会在自定义版本号基础上加1
     * @return $this
     */
    public function setCacheConfig(string $cache_key, int|null $ttl = null, bool $inc_version = false, int $version = 0): self
    {
        $this-&amp;gt;cache_key_ori = $cache_key;
        $this-&amp;gt;setVersionKey();

        // 若指定版本号则设置
        !empty($version) &amp;amp;&amp;amp; $this-&amp;gt;setVersion($version);

        // 获取当前版本号 +1
        $version = $this-&amp;gt;getVersion();
        $inc_version &amp;amp;&amp;amp; $this-&amp;gt;setVersion($version + 1);

        // 设置缓存时间
        !is_null($ttl) &amp;amp;&amp;amp; $this-&amp;gt;ttl = $ttl;

        $this-&amp;gt;cache_key = $this-&amp;gt;generateCacheKey($this-&amp;gt;getVersion());
        return $this;
    }

    /**
     * 生成缓存键名
     * @param int $version
     * @return string
     */
    protected function generateCacheKey(int $version): string
    {
        return CacheEnum::CACHE_PREFIX . &apos;:&apos; . $this-&amp;gt;getEnv() . &apos;:&apos; . $this-&amp;gt;cache_key_ori . &apos;:&apos; . $version;
    }

    public function getEnv(): string
    {
        return app(&apos;env&apos;)-&amp;gt;get(&apos;app_env&apos;) ?? &apos;dev&apos;;
    }

    /**
     * 获取有效期
     * @return int|null
     */
    public function getTtl(): int|null
    {
        return $this-&amp;gt;ttl;
    }

    /**
     * 获取缓存键名
     * @return string
     */
    public function getCacheKey(): string
    {
        return $this-&amp;gt;cache_key;
    }

    /**
     * 删除缓存
     * @param array $version_list 版本号列表
     * @return bool
     */
    public function delete(array $version_list): bool
    {
        if (!$version_list) {
            return true;
        }
        foreach ($version_list as $version) {
            $cache_key = $this-&amp;gt;generateCacheKey($version);
            Cache::delete($cache_key);
        }
        return true;
    }

    /**
     * 获取历史版本缓存 不包含当前版本
     * @return array
     */
    public function getHistoryVersion(): array
    {
        $version_list = [];
        for ($i = CacheEnum::DEFAULT_VERSION; $i &amp;lt; $this-&amp;gt;getVersion(); $i++) {
            $cache_key = $this-&amp;gt;generateCacheKey($i);
            if (Cache::has($cache_key)) {
                $version_list[] = $i;
            }
        }
        return $version_list;
    }

    /**
     * 手动设置缓存版本号
     * @param int $version
     * @return $this
     */
    protected function setVersion(int $version): self
    {
        $this-&amp;gt;version = $version;
        $this-&amp;gt;is_set_version = true;
        return $this;
    }


    /**
     * 获取版本号键名
     * @return string
     */
    protected function getVersionKey(): string
    {
        return $this-&amp;gt;version_key;
    }


    /**
     * 设置缓存版本键名
     * @return $this
     */
    protected function setVersionKey(): self
    {
        $this-&amp;gt;version_key = CacheEnum::CACHE_VERSION_PREFIX . &apos;:&apos; . $this-&amp;gt;getEnv() . &apos;:&apos; . $this-&amp;gt;cache_key_ori;
        return $this;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 使用方法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;首先注入帮助类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#[Inject]
protected CacheHelper $cacheHelper;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;使用闭包方式设置缓存 若缓存不存在执行闭包返回结果（适合返回结果处理比较简单的情况 这样简洁一些）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$level = 1;
$cache_key = CacheEnum::PRODUCT_CATEGORY_LIST_CACHE_KEY . &quot;_{$level}&quot;;
return $this-&amp;gt;cacheHelper-&amp;gt;setCacheConfig($cache_key, 3600 * 24 * 4)-&amp;gt;remember(function () use ($params) {
    return $this-&amp;gt;getListByLevel($params);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;手动判断缓存 是否为空 处理业务（适合逻辑比较复杂的情况）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$cache_key = CacheEnum::PRODUCT_DETAIL_GET_LINK_CACHE_KEY . &quot;_{$type}&quot;;
$cache = $this-&amp;gt;cacheHelper-&amp;gt;setCacheConfig($cache_key, 3600 * 24);

if (!empty($cache_data = $cache-&amp;gt;getCacheData())) {
    return $cache_data;
}

// todo 处理业务 获取结果
$return_data = [&apos;aaa&apos;];

// 设置缓存
!empty($return_data) &amp;amp;&amp;amp; $cache-&amp;gt;setCacheData($return_data);

// 返回结果
return $return_data;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 扩展用法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;自增缓存版本号&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$cache = $this-&amp;gt;cacheHelper-&amp;gt;setCacheConfig($cache_key, 3600 * 24, true);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;指定版本号缓存（如果指定版本号缓存 第三个参数传false，如果传true 以下示例的版本号会变成1010）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$cache = $this-&amp;gt;cacheHelper-&amp;gt;setCacheConfig($cache_key, 3600 * 24, false, 1009);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;获取当前版本之前的历史版本号 （初始版本号为1001 以下示例为获取 1001～1008版本的缓存）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$cache = $this-&amp;gt;cacheHelper-&amp;gt;setCacheConfig($cache_key, 3600 * 24, false, 1009);
$history_version_list = $cache-&amp;gt;getHistoryVersion();
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;删除指定版本号列表的缓存&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;cacheHelper-&amp;gt;setCacheConfig($cache_key)-&amp;gt;delete([1001,1002]);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>💄Docker常用命令</title><link>https://www.corgi37.top/posts/docker3/</link><guid isPermaLink="true">https://www.corgi37.top/posts/docker3/</guid><pubDate>Tue, 17 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;基本类&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/reference/&quot;&gt;官方帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;启动 docker&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;关闭docker&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;重新启动 docker&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;docker设置自启动&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查看docker运行状态&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl status docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查看docker版本号等信息&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker version
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;显示docker的系统信息&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;包括镜像和容器的数量等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker info
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;docker帮助&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker --help
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;镜像类&lt;/h3&gt;
&lt;h4&gt;查看本地镜像&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/image/&quot;&gt;官网帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REPOSITORY					镜像的仓库源&lt;/li&gt;
&lt;li&gt;TAG							       镜像的标签&lt;/li&gt;
&lt;li&gt;IMAGE ID                          镜像的id&lt;/li&gt;
&lt;li&gt;CREATED                          镜像的创建时间&lt;/li&gt;
&lt;li&gt;SIZE                                   镜像的大小&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h5&gt;扩展命令&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-a&lt;/td&gt;
&lt;td&gt;列出所有镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-q&lt;/td&gt;
&lt;td&gt;只显示镜像的id&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;搜索镜像&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/search/&quot;&gt;官网帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker search
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;扩展命令&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-f &amp;lt;过滤条件&amp;gt;, --filter&lt;/td&gt;
&lt;td&gt;过滤镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使用 Go 模板进行漂亮打印搜索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--limit &amp;lt;限制数量&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限制最大搜索结果数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--no-trunc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示完整的镜像描述&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;举例&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;通过收藏数量过滤, 搜索STARS数量大于等于5000的 且是官方构建的镜像&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;is-official (boolean - true 或 false) - 镜像是否是官方的&lt;/li&gt;
&lt;li&gt;stars (int - number) - 星星数&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker search mysql --filter=stars=5000 --filter is-official=true&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;拉取镜像&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/pull/&quot;&gt;官网帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker pull
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;扩展命令&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-a, --all-tags&lt;/td&gt;
&lt;td&gt;下载仓库中所有标签的镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--disable-content-trust&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;跳过镜像验证 默认true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--platform&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置平台&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-q, --quiet&lt;/td&gt;
&lt;td&gt;禁止详细输出&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;举例&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;以下三个命令是一样的含义，不指定tag默认拉取最新版本&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker pull mysql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker pull docker.io/mysql:latest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker pull mysql:latest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;拉取amd64平台的镜像&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker pull --platform=linux/amd64 jlesage/firefox&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;删除镜像&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/reference/cli/docker/image/rm/&quot;&gt;官网帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker rmi
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;扩展命令&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-f, --force&lt;/td&gt;
&lt;td&gt;是否强制删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--no-prune&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不删除未标记的父类&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;举例&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;删除镜像可以使用镜像名或镜像Id, 若镜像在运行则会报错&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker rmi  镜像名/镜像ID&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;强制删除多个镜像，若使用该镜像的容器在运行也会删除&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker rmi -f 镜像名/镜像ID 镜像名/镜像ID 镜像名/镜像ID&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;删除全部镜像，以下任意一个命令都可以&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker rmi -f $(docker images -aq)
docker rmi -f `docker images -aq`
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;加载镜像&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/reference/cli/docker/image/load/&quot;&gt;官网帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker load
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;扩展命令&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-i, --input&lt;/td&gt;
&lt;td&gt;从 tar 存档文件读取，而不是标准输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-q, --quiet&lt;/td&gt;
&lt;td&gt;禁止加载输出&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;举例&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;从标准输入加载镜像&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker load &amp;lt; busybox.tar.gz&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从文件中加载镜像&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker load -i fedora.tar&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;保存镜像&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/reference/cli/docker/image/save/&quot;&gt;官网帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker save
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;扩展命令&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-o, --output&lt;/td&gt;
&lt;td&gt;写入文件，而不是标准输入&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;举例&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;从标准输入保存镜像&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker save ubuntu:latest &amp;gt; /root/ubuntu.tar&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;保存到指定目录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker save -o ./ubuntu-latest.tar ubuntu:latest&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;容器类&lt;/h3&gt;
&lt;h4&gt;查看正在运行的容器&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/reference/cli/docker/container/ls/&quot;&gt;官网帮助文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;扩展命令&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-a, --all&lt;/td&gt;
&lt;td&gt;显示所有容器（默认只显示正在运行的容器）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-f, --filter&lt;/td&gt;
&lt;td&gt;根据提供的条件过滤输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使用自定义模板格式化输出： &amp;lt;br/&amp;gt;&apos;table&apos;：以表格格式打印输出，包含列标题（默认） &amp;lt;br/&amp;gt;&apos;table TEMPLATE&apos;：使用给定的 Go 模板以表格格式打印输出 &amp;lt;br/&amp;gt;&apos;json&apos;：以 JSON 格式打印 &amp;lt;br/&amp;gt;&apos;TEMPLATE&apos;：使用给定的 Go 模板打印输出。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-n, --last&lt;/td&gt;
&lt;td&gt;显示最后创建的 n 个容器（包括所有状态），默认值-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-l, --latest&lt;/td&gt;
&lt;td&gt;显示最新创建的容器（包括所有状态）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--no-trunc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不要截断输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-q, --quiet&lt;/td&gt;
&lt;td&gt;只显示容器ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-s, --size&lt;/td&gt;
&lt;td&gt;显示文件大小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;举例&lt;/h5&gt;
&lt;h6&gt;显示所有容器&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;显示所有容器（包含所有状态的）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/p&gt;
&lt;h6&gt;过滤器&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;–f 的格式是键=值对。如果有多个过滤器，则传递多个标志&lt;/p&gt;
&lt;p&gt;查找正在运行的且名字包含local-mysql的容器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps -f status=running -f name=&quot;local-mysql&quot;&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;过滤器&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;容器ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;容器名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;label&lt;/td&gt;
&lt;td&gt;一个任意字符串，代表一个键或者一个键值对。表示为&amp;lt;key&amp;gt; 或 &amp;lt;key&amp;gt;=&amp;lt;value&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exited&lt;/td&gt;
&lt;td&gt;一个整数，代表容器的退出码。仅在与 –all 选项一起使用时才有意义。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;td&gt;状态为 created（已创建）、restarting（正在重启）、running（正在运行）、removing（正在移除）、paused（已暂停）、exited（已退出）或 dead（已死亡）其中一种。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ancestor&lt;/td&gt;
&lt;td&gt;筛选出共享给定镜像作为祖先的容器。表示为&amp;lt;image-name&amp;gt;[:&amp;lt;tag&amp;gt;]、&amp;lt;image id&amp;gt;或&lt;a href=&quot;mailto:image@digest&quot;&gt;image@digest&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;before 或 since&lt;/td&gt;
&lt;td&gt;筛选在给定容器 ID 或名称之前或之后创建的容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volume&lt;/td&gt;
&lt;td&gt;筛选挂载了给定卷或绑定挂载的正在运行的容器。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;network&lt;/td&gt;
&lt;td&gt;筛选连接到给定网络的正在运行的容器。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;publish 或 expose&lt;/td&gt;
&lt;td&gt;筛选发布或暴露给定端口的容器。表示为&amp;lt;port&amp;gt;[/&amp;lt;proto&amp;gt;]或&amp;lt;startport-endport&amp;gt;/[&amp;lt;proto&amp;gt;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;health&lt;/td&gt;
&lt;td&gt;根据容器的健康检查状态筛选容器。可能的状态有 starting（启动中）、healthy（健康）、unhealthy（不健康）或 none（无）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isolation&lt;/td&gt;
&lt;td&gt;仅适用于 Windows 守护进程。值为 default、process 或 hyperv 之一。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is-task&lt;/td&gt;
&lt;td&gt;筛选作为服务&quot;任务&quot;的容器。布尔选项（true 或 false）。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h6&gt;自定义输出格式&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;自定义输出格式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps -a --format &quot;table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{printf \&quot;%.40s\&quot; .Ports}}\t{{.Status}}&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker ps --format &quot;{{.ID}}: {{.Command}} {{.RunningFor}}&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker ps --format json&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;按照Go模板的方式会涉及到以下占位符：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;过滤器&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.ID&lt;/td&gt;
&lt;td&gt;容器ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Image&lt;/td&gt;
&lt;td&gt;镜像ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Command&lt;/td&gt;
&lt;td&gt;引用的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.CreatedAt&lt;/td&gt;
&lt;td&gt;容器创建的时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.RunningFor&lt;/td&gt;
&lt;td&gt;自容器启动以来的经过时间。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Ports&lt;/td&gt;
&lt;td&gt;暴露的端口。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.State&lt;/td&gt;
&lt;td&gt;容器状态（例如：”created”（已创建）、”running”（运行中）、”exited”（已退出））。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Status&lt;/td&gt;
&lt;td&gt;容器状态，包括持续时间与健康状态的详细信息。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Size&lt;/td&gt;
&lt;td&gt;容器磁盘大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Names&lt;/td&gt;
&lt;td&gt;容器名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Labels&lt;/td&gt;
&lt;td&gt;分配给容器的所有标签。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Label&lt;/td&gt;
&lt;td&gt;此容器特定标签的值。例如 &apos;{{.Label &quot;com.docker.swarm.cpu&quot;}}&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Mounts&lt;/td&gt;
&lt;td&gt;在此容器中挂载的卷的名称。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.Networks&lt;/td&gt;
&lt;td&gt;连接到这个容器的网络的名称。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h6&gt;查看最新创建的n个容器&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;该选项后面必须加一个数字，否则会报错。该选项显示-n后面数字个容器，无论容器处于什么状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps -n 3&lt;/code&gt;&lt;/p&gt;
&lt;h6&gt;查看最新创建的1个容器&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;该选项显示最新创建的容器，无论该容器处于什么状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps -l&lt;/code&gt;&lt;/p&gt;
&lt;h6&gt;不截断输出&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;容器ID完整输出了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps --no-trunc&lt;/code&gt;&lt;/p&gt;
&lt;h6&gt;精简输出&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;只输出容器ID&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps -q&lt;/code&gt;&lt;/p&gt;
&lt;h6&gt;磁盘容量&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;容器列表的最右侧增加SIZE列，显示两种不同的磁盘容量大小&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps -s&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/image-20250218104008025.png!v1&quot; alt=&quot;image-20250218104008025&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中，前面的数字表示用于每个容器的可写层的磁盘数据量。 后面括号中“virtual” 表示的是容器使用的只读镜像数据以及可写层所用的磁盘空间总量。多个容器可能会共享一些或全部的只读镜像数据。从同一镜像启动的两个容器共享 100% 的只读数据，而具有共同层的不同镜像的两个容器则共享这些共同层。因此，不能简单地将虚拟大小相加。这可能会大大高估总磁盘使用量。&lt;/p&gt;
&lt;p&gt;所有正在运行的容器在磁盘上使用的总磁盘空间是每个容器的 size 和 virtual size 值的组合。如果多个容器从完全相同的镜像启动，这些容器的总磁盘大小将是容器的总和（size of containers）加上一个镜像大小（virtual size – size）。&lt;/p&gt;
&lt;p&gt;以上显示的磁盘空间占用情况，不包括以下五种方式：&lt;/p&gt;
&lt;p&gt;存储在日志驱动程序中的日志文件占用的磁盘空间。如果你的容器生成大量日志数据且未配置日志轮换，则这可能是非常重要的。&lt;/p&gt;
&lt;p&gt;容器使用的卷和绑定挂载。
用于容器的配置文件的磁盘空间，这些文件通常很小。
写入磁盘的内存（如果启用了交换）。
如果你正在使用实验性的检查点/恢复功能，则还包括检查点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🚀Docker概述</title><link>https://www.corgi37.top/posts/docker1/</link><guid isPermaLink="true">https://www.corgi37.top/posts/docker1/</guid><pubDate>Sun, 15 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;docker为什么出现？&lt;/h3&gt;
&lt;p&gt;一款产品： 开发–上线 两套环境！应用环境，应用配置！&lt;/p&gt;
&lt;p&gt;开发 — 运维。 问题：我在我的电脑上可以允许！版本更新，导致服务不可用！对于运维来说考验十分大？&lt;/p&gt;
&lt;p&gt;环境配置是十分的麻烦，每一个及其都要部署环境(集群Redis、ES、Hadoop…) !费事费力。&lt;/p&gt;
&lt;p&gt;发布一个项目( jar + (Redis MySQL JDK ES) ),项目能不能带上环境安装打包！&lt;/p&gt;
&lt;p&gt;之前在服务器配置一个应用的环境 Redis MySQL JDK ES Hadoop 配置超麻烦，不能够跨平台。&lt;/p&gt;
&lt;p&gt;开发环境Windows，最后发布到Linux！&lt;/p&gt;
&lt;p&gt;传统：开发jar，运维来做！&lt;/p&gt;
&lt;p&gt;现在：开发打包部署上线，一套流程做完！&lt;/p&gt;
&lt;p&gt;安卓流程：java — apk —发布（应用商店）一 张三使用apk一安装即可用！&lt;/p&gt;
&lt;p&gt;docker流程： java-jar（环境） — 打包项目帯上环境（镜像） — ( Docker仓库：商店）-----&lt;/p&gt;
&lt;p&gt;Docker给以上的问题，提出了解决方案！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker_1.png!v1&quot; alt=&quot;docker_1&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker的思想就来自于集装箱！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;多个应用(端口冲突) – 原来都是交叉的！&lt;/p&gt;
&lt;p&gt;隔离：Docker核心思想！打包装箱！每个箱子是互相隔离的。&lt;/p&gt;
&lt;p&gt;Docker通过隔离机制，可以将服务器利用到极致！&lt;/p&gt;
&lt;p&gt;本质：所有的技术都是因为出现了一些问题，我们需要去解决，才去学习！&lt;/p&gt;
&lt;h3&gt;docker历史&lt;/h3&gt;
&lt;p&gt;2010年，几个的年轻人，就在美国成立了一家公司 &lt;code&gt;dotcloud&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;做一些pass的云计算服务！LXC（Linux Container容器）有关的容器技术！&lt;/p&gt;
&lt;p&gt;Linux Container容器是一种内核虚拟化技术，可以提供轻量级的虚拟化，以便隔离进程和资源。&lt;/p&gt;
&lt;p&gt;他们将自己的技术（容器化技术）命名就是 Docker&lt;/p&gt;
&lt;p&gt;Docker刚刚延生的时候，没有引起行业的注意！dotCloud，就活不下去！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;开源&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2013年，Docker开源！&lt;/p&gt;
&lt;p&gt;越来越多的人发现docker的优点！火了。Docker每个月都会更新一个版本！&lt;/p&gt;
&lt;p&gt;2014年4月9日，Docker1.0发布！&lt;/p&gt;
&lt;p&gt;docker为什么这么火？十分的轻巧！&lt;/p&gt;
&lt;p&gt;在容器技术出来之前，我们都是使用虚拟机技术！&lt;/p&gt;
&lt;p&gt;虚拟机：在window中装一个VMware，通过这个软件我们可以虚拟出来一台或者多台电脑！笨重！&lt;/p&gt;
&lt;p&gt;虚拟机也属于虚拟化技术，Docker容器技术，也是一种虚拟化技术！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VMware : linux centos 原生镜像（一个电脑！） 隔离、需要开启多个虚拟机！ 几个G 几分钟&lt;/li&gt;
&lt;li&gt;docker: 隔离，镜像（最核心的环境 4m + jdk + mysql）十分的小巧，运行镜像就可以了！小巧！ 几个M 秒级启动！&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;聊聊Docker&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Docker基于Go语言开发的！开源项目！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;官网：https://www.docker.com/&lt;/p&gt;
&lt;p&gt;文档：https://docs.docker.com/&lt;/p&gt;
&lt;p&gt;仓库：https://hub.docker.com/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;虚拟技术与容器化技术对比&lt;/h3&gt;
&lt;h4&gt;之前的虚拟机技术&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker_2.png!v1&quot; alt=&quot;docker_2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;资源占用多&lt;/li&gt;
&lt;li&gt;冗余步骤多&lt;/li&gt;
&lt;li&gt;启动慢&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h4&gt;容器化技术&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker_3.png!v1&quot; alt=&quot;docker_3&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;并非模拟完整的操作系统&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;比较&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;传统虚拟机，虚拟硬件，运行完整的操作系统，然后安装运行软件&lt;/li&gt;
&lt;li&gt;容器的应用直接运行在宿主机的内核，没有虚拟硬件，轻便&lt;/li&gt;
&lt;li&gt;每个容器之间互相隔离，每个容器内都有一个属于自己的文件系统，互不影响&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;DevOps（开发、运维）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;应用更快速的交付和部署&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;传统：一对帮助文档，安装程序。&lt;/p&gt;
&lt;p&gt;Docker：打包镜像发布测试一键运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;更便捷的升级和扩缩容&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用了 Docker之后，我们部署应用就和搭积木一样&lt;/p&gt;
&lt;p&gt;项目打包为一个镜像，扩展服务器A！服务器B&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;更简单的系统运维&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在容器化之后，我们的开发，测试环境都是高度一致的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;更高效的计算资源利用&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker是内核级别的虚拟化，可以在一个物理机上可以运行很多的容器实例！服务器的性能可以被压榨到极致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;Docker 是一个开源的应用容器引擎，基于 &lt;strong&gt;Go 语言&lt;/strong&gt; 并遵从 Apache2.0 协议开源。&lt;/p&gt;
&lt;p&gt;Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中，然后发布到任何流行的 Linux 机器上，也可以实现虚拟化。&lt;/p&gt;
&lt;p&gt;容器是完全使用沙箱机制，相互之间不会有任何接口（类似 iPhone 的 app）,更重要的是容器性能开销极低。&lt;/p&gt;
&lt;p&gt;Docker 从 17.03 版本之后分为 CE（Community Edition: 社区版） 和 EE（Enterprise Edition: 企业版），我们用社区版就可以了。&lt;a href=&quot;https://docs.docker.com/&quot;&gt;官网地址&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Docker的基本组成&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker_4.png!v1&quot; alt=&quot;docker_4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;镜像（image)：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;docker镜像就好比是一个目标，可以通过这个目标来创建容器服务，tomcat镜像==&amp;gt;run==&amp;gt;容器（提供服务器），通过这个镜像可以创建多个容器（最终服务运行或者项目运行就是在容器中的）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;容器(container)：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker利用容器技术，独立运行一个或者一组应用，通过镜像来创建的.&lt;/p&gt;
&lt;p&gt;启动，停止，删除，基本命令&lt;/p&gt;
&lt;p&gt;目前就可以把这个容器理解为就是一个简易的 Linux系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;仓库(repository)：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;仓库就是存放镜像的地方！&lt;/p&gt;
&lt;p&gt;仓库分为公有仓库和私有仓库。(很类似git)&lt;/p&gt;
&lt;p&gt;Docker Hub是国外的。&lt;/p&gt;
&lt;p&gt;阿里云…都有容器服务器(配置镜像加速!)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;应用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Web 应用的自动化打包和发布。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化测试和持续集成、发布。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在服务型环境中部署和调整数据库或其他的后台应用。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;docker run 的运行流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker_5.jpg!v1&quot; alt=&quot;docker_5&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;底层原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;docker 的工作原理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Docker 是一个 Client-Server 结构的系统, Docker的守护进程运行在主机上. 通过&lt;strong&gt;Socket&lt;/strong&gt;从客户端访问!&lt;/p&gt;
&lt;p&gt;DockerServer 接收到 Docker-Client 的指令, 就会执行这个命令!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker_6.png!v1&quot; alt=&quot;docker_6&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker 为什么比 VM 快?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Docker 有着比虚拟机更少的抽象层&lt;/li&gt;
&lt;li&gt;Docker 利用的是宿主机的内核, VM 需要的是Guest OS&lt;/li&gt;
&lt;li&gt;新建一个容器，docker不需要像虚拟机一样重新加载一个操作系统内核，避免引导。虚拟机加载Guest OS，分钟级别，而docker利用宿主机的操作系统，省略复杂过程，秒级。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>🍟Docker安装</title><link>https://www.corgi37.top/posts/docker2/</link><guid isPermaLink="true">https://www.corgi37.top/posts/docker2/</guid><pubDate>Sun, 15 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Linux环境安装&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1.Linux要求内核3.0以上&lt;/p&gt;
&lt;p&gt;2.CentOS 7以上&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_1.png!v1&quot; alt=&quot;docker2_1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/centos/&quot;&gt;官网文档&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 卸载旧版本&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 安装基础包&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;yum install -y yum-utils \
  device-mapper-persistent-data \
  lvm2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 设置国内阿里镜像&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;yum-config-manager \
    --add-repo \
    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 更新软件包索引&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;yum makecache在centos8已经没有fast的命令了  所以yum makecache fast不能使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;yum makecache
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 安装docker&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;docker-ce 社区版 而ee是企业版&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;yum install docker-ce docker-ce-cli containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6. 启动docker&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7. 测试&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker version #查看版本
docker run hello-world #运行hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_2.png!v1&quot; alt=&quot;docker2_2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;了解：卸载docker&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;yum remove docker-ce docker-ce-cli containerd.io
rm -rf /var/lib/docker
rm -rf /var/lib/containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8. 配置加速源&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;进入阿里云官网搜索&lt;code&gt;容器镜像服务&lt;/code&gt;进入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_3.png!v1&quot; alt=&quot;docker2_3&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;点击控制台&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_4.png!v1&quot; alt=&quot;docker2_4&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;选择镜像加速&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_5.png!v1&quot; alt=&quot;docker2_5&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/docker

sudo tee /etc/docker/daemon.json &amp;lt;&amp;lt;-&apos;EOF&apos;
{
  &quot;registry-mirrors&quot;: [&quot;https://deytserb.mirror.aliyuncs.com&quot;]
}
EOF

sudo systemctl daemon-reload

sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9. 设置开机自启&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl list-units --type=service #查看已启动的服务列表
systemctl enable docker.service     #设置docker开机自启
systemctl list-unit-files | grep enable   #查看是否设置成功
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows环境安装&lt;/h3&gt;
&lt;h4&gt;1. 勾选Hyper-V&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker支持Windows 10 Pro且需要开启Hyper-v&lt;/p&gt;
&lt;p&gt;控制面板-&amp;gt;程序和功能-&amp;gt;启用或关闭Windows功能&lt;/p&gt;
&lt;p&gt;将下图红框部分选中点击确定&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_6.png!v1&quot; alt=&quot;docker2_6&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2. 下载docker桌面版&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;在docker官网&lt;a href=&quot;https://docs.docker.com/docker-for-windows/install/&quot;&gt;下载docker桌面版&lt;/a&gt;并安装，安装成功后桌面会出现这个图标&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_7.png!v1&quot; alt=&quot;docker2_7&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3. 注册docker hub并登录&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://hub.docker.com/&quot;&gt;注册dockerhub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4. 配置加速源&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;进入阿里云官网搜索&lt;code&gt;容器镜像服务&lt;/code&gt;进入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_8.png!v1&quot; alt=&quot;docker2_8&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;点击控制台&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_9.png!v1&quot; alt=&quot;docker2_9&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;选择镜像加速，复制镜像加速地址&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_10.png!v1&quot; alt=&quot;docker2_10&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;打开DockerDesktop ,点击设置&amp;gt;Docker Engine，在registry-mirrors一栏后加入阿里云复制的镜像加速地址即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_11.png!v1&quot; alt=&quot;docker2_11&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/docker2_12.png!v1&quot; alt=&quot;docker2_12&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5. 查看Docker版本&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;windows命令行win+r，输入cmd并回车进入终端，输入查看版本，无异常报错信息则安装成功&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6. 登录Docker&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;终端输入命令登录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker login docker.io
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>🚀线性表的类型定义</title><link>https://www.corgi37.top/posts/datastructure6/</link><guid isPermaLink="true">https://www.corgi37.top/posts/datastructure6/</guid><pubDate>Wed, 11 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;🧨抽象数据类型线性表的定义&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_1.png!v1&quot; alt=&quot;datastructure6_1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;🎃基本操作&lt;/h3&gt;
&lt;h4&gt;🎍初始化&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;InitList(&amp;amp;L) (intialization List)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：构造一个空的线性表L&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🎎销毁线性表&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;DestoryList(&amp;amp;L)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：销毁线性表L&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🥼线性表置空/清空线性表&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;ClearList(&amp;amp;L)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：将线性表L重置为空表。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🖼判断线性表是否为空&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;ListEmpty(L)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：若线性表L为空表，则返回true;否则返回false&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;✨判断元素个数(表长)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;ListLength(L)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：返回线性表L中的数据元素个数。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🎐线性表的按值查找&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;GetElem(L,i,&amp;amp;e)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在，1&amp;lt;=i&amp;lt;=ListLength(L)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：用e返回线性表L中第i个数据元素的值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LocateElem(L,e,Compare())&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在，compare()是数据元素判定函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：返回L中第一个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值为0。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🧵查找某个元素的前驱&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;PriorElem(L,cur_e,&amp;amp;pre_e)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：若cur_e是L的数据元素，且不是第一个，则用pre_e返回他的前驱，否则操作失败。pre_e无意义。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🧵查找某个元素的后继&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;NextElem(L,cur_e,&amp;amp;next_e)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：若cur_e是L的数据元素，且不是第最后一个，则用next_e返回他的后继，否则操作失败，next_e无意义。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🥽插入元素&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;ListInsert(&amp;amp;L, i, e)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在，1&amp;lt;=i&amp;lt;=ListLength(L)+1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：在L的第i个位置之前插入新的数据元素e,L的长度加一。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_2.png!v1&quot; alt=&quot;datastructure6_2&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;🎑删除某个元素&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;ListDelete(&amp;amp;L,i,&amp;amp;e)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在，1&amp;lt;=i&amp;lt;=ListLength(L)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：删除L的第i个数据元素，并用e返回其值，L的长度减一。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_3.png!v1&quot; alt=&quot;datastructure6_3&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;🛒对线性表每个元素做操作&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;ListTraverse(&amp;amp;L, visited())&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始条件：线性表L已经存在&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作结果：依次对线性表中每个元素调用visited()&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;🥓插入算法&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_4.png!v1&quot; alt=&quot;datastructure6_4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(在线性表a3位置之前插入一个新元素e(e=6))&lt;/p&gt;
&lt;p&gt;插入位置在最后在线性表的最后添加一个元素不需要移动直接添加&lt;/p&gt;
&lt;p&gt;插入位置在最前在原线性表的第1个元素之前插入一个新的元素，线性表的所有元素都要移动移动次数最多&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;插入位置中间如下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_5.gif!v1&quot; alt=&quot;datastructure6_5&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_6.png!v1&quot; alt=&quot;datastructure6_6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_7.png!v1&quot; alt=&quot;datastructure6_7&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;🥫删除算法&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure6_8.png!v1&quot; alt=&quot;datastructure6_8&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;🧈小结&lt;/h3&gt;
&lt;p&gt;查找、插入、删除的平均算法复杂度为&lt;strong&gt;O(n)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;空间复杂度显然顺序表操作没有占用辅助空间算法的空间复杂度&lt;strong&gt;O(1)&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🍠线性表的优点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;存储密度大(结点本身所占用的空间/结点结构所占存储量=1)无需为表示表中元素之间的逻辑关系，而增加额外的存储空间&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可以随机存取表中任意位置的元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;🍣线性表的缺点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;插入、删除某一元素需移动大量元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当线性表长度变化较大时，难以确定存储空间的容量，&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据元素的个数不能自由扩充(存储空间不灵活)&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>🚀线性表的定义和特点</title><link>https://www.corgi37.top/posts/datastructure5/</link><guid isPermaLink="true">https://www.corgi37.top/posts/datastructure5/</guid><pubDate>Wed, 11 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;🧨线性表的定义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;由n(n≥O)个数据特性相同的元素构成的有限序列称为线性表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_1.png!v1&quot; alt=&quot;datastructure5_1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;🎊线性表的特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线性表中元素的个数n(n≥O)定义为线性表的长度，n=O时称为空表&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据元素的个数n定义为表的长度&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当n=0时称为空表&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;将非空的线性表(n&amp;gt;0)记作：(a₁, a₂, ... an)&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;这里的数据元素ai(1≤i≤n)只是个抽象的符号，其具体含义在不同情况下可以不同。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在非空的线性表，有且仅有一个开始结点a1,它没有直接前趋，而仅有一个直接后继a2;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有且仅有一个终端结点an,它没有直接后继，而仅有一个直接前趋an-1;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;其余的内部结点ai,(2&amp;lt;i&amp;lt;n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🎨线性表的例子&lt;/h3&gt;
&lt;p&gt;26个英文字母组成的英文表：(A, B, C, ...，Z)；学生信息表；学校历年电脑数量；12星座。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一线性表中的元素必定具有相同的特性，数据元素之间关系是线性的。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;线性表是一种典型的线性结构。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;案例引入&lt;/h3&gt;
&lt;h4&gt;案例1：&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_2.png!v1&quot; alt=&quot;datastructure5_2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;逻辑结构抽象为线性表存储结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_3.png!v1&quot; alt=&quot;datastructure5_3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_4.png!v1&quot; alt=&quot;datastructure5_4&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;案例2：&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_5.png!v1&quot; alt=&quot;datastructure5_5&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在处理这样的稀疏多项式时，就要用长度为20001的线性表来表示，而表中只有三个元素，将造成很大的存储空间浪费，因此我们可以改变元素设定，对多项式的每一项用(系数，指数)唯一确定。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_6.png!v1&quot; alt=&quot;datastructure5_6&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;稀疏多项式的运算：&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;加法&lt;/p&gt;
&lt;p&gt;A=((7,0),(3,1),(9,8),(5,17))[4项]&lt;/p&gt;
&lt;p&gt;B=((8,1),(22,7),(-9,8),)[3项]&lt;/p&gt;
&lt;p&gt;创建一个新的多项式c用来存放a与b和&lt;/p&gt;
&lt;p&gt;分别从头遍历比较a和b的每一项&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;结果为((7,0),(11,1),(22,7),(5,17))&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;指数相同，对应系数相加，若其和不为零，则在c中增加一个新项&lt;/p&gt;
&lt;p&gt;指数不相同，则将指数较小的项复制到c中&lt;/p&gt;
&lt;p&gt;一个多项式已遍历完毕时，将另一个剩余项依次复制到c中即可&lt;/p&gt;
&lt;p&gt;顺序存储结构存在问题&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;存储空间分配不灵活；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;运算的空间复杂度高&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;尝试链式存储结构(不需要额外的空间只利用已有的空间)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_7.png!v1&quot; alt=&quot;datastructure5_7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_8.gif!v1&quot; alt=&quot;datastructure5_8&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;案例3：&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_9.png!v1&quot; alt=&quot;datastructure5_9&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_10.png!v1&quot; alt=&quot;datastructure5_10&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图书表抽象为线性表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;表中每本图书抽象线性表中数据元素&lt;/p&gt;
&lt;p&gt;可以用顺序存储结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_11.png!v1&quot; alt=&quot;datastructure5_11&quot; /&gt;&lt;/p&gt;
&lt;p&gt;或链表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure5_12.png!v1&quot; alt=&quot;datastructure5_12&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比较这两种存储结构的优缺点根据实际情况，选择适当的存储结构，实现此存储结构上的基本操作，利用基本操作完成功能。&lt;/p&gt;
</content:encoded></item><item><title>🎍渐进时间复杂度</title><link>https://www.corgi37.top/posts/datastructure4/</link><guid isPermaLink="true">https://www.corgi37.top/posts/datastructure4/</guid><pubDate>Wed, 11 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;🎉渐进时间复杂度&lt;/h3&gt;
&lt;p&gt;若有某个辅助函数f(n)，使得当n趋近于无穷大时，T(n)/f(n)的极限值为&lt;strong&gt;不等于零的常数&lt;/strong&gt;，则称f(n)是T(n)的同数量级函数。记作T(n) = O(f(n)), 称O(f(n))为&lt;strong&gt;算法的渐进时间复杂度&lt;/strong&gt;（O是数量级的符号），简称&lt;strong&gt;时间复杂度&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于稍微复杂一些的算法，计算出算法中所有语句的频度通常是比较困难的。通常，算法的执行时间是随问题规模增长而增长的，因此对算法的评价通常只需考虑其随问题规模增长的趋势。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;n -&amp;gt; ∞时，T(n) / n³ -&amp;gt; 2,这表示n充分大时 ，T(n)于n³是同阶或同数量级，引入大“O”记号，则T（n）可记作：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;T(n) = O(n³)&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这就是求解矩阵相乘问题的算法的&lt;strong&gt;渐进时间复杂度&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一般情况下，不必计算所有操作的执行次数，而值考虑算法中基本操作执行次数，它是问题规模n的某个函数，用T（n）表示。&lt;/p&gt;
&lt;p&gt;算法中&lt;strong&gt;基本语句重复执行的次数&lt;/strong&gt;是&lt;strong&gt;问题规模n&lt;/strong&gt;的某个函数f(n)，算法的时间量度记作：&lt;strong&gt;T(n) = O(f(n))&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基本语句：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;算法中重复执行次数和算法的执行时间成正比的语句；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对算法运行的时间贡献最大；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行次数最多。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题规模n：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;n越大算法的执行时间越长&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;排序：n为记录数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;矩阵：n为矩阵的阶数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多项式：n为多项式的项数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集合：n为元素个数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树：n为树的结点个数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图：n为图的顶点数或边数&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🎎分析算法时间复杂度的基本方法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;找出&lt;strong&gt;语句频度最大&lt;/strong&gt;的那条语句作为&lt;strong&gt;基本语句&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算&lt;strong&gt;基本语句的频度&lt;/strong&gt;得到问题规模n的某个函数f(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取其数量级用符号&quot;&lt;strong&gt;O&lt;/strong&gt;&quot;表示&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_1.png!v1&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;举例1：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;x = 0; y = 0;															//执行一次
for (int k = 0; k &amp;lt; n; k ++ )							//外层语句判断 n + 1次
	x ++;																		//循环体执行n次
for (int i = 0; i &amp;lt; n; i ++ )							//外层语句判断 n + 1次
	for (int j = 0; j &amp;lt; n; j ++ )						//循环体内的外层判断语句n+1次 循环体n次所以是n(n+1)次
  	y ++;																	//循环体内为n*n
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;通常循环体嵌套最深的往往是执行次数最多的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以该例的时间复杂度为&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;T(n) = O(n²)&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;忽略所有低次幂项和最高次幂的系数，这样可以简化算法分析，也体现出了增长率的含义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度是由嵌套最深层语句的频度决定的。&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;举例2：&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_2.png!v1&quot; alt=&quot;datastructure4_2&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;举例3：&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_3.png!v1&quot; alt=&quot;datastructure4_3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_4.png!v1&quot; alt=&quot;datastructure4_4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_5.png!v1&quot; alt=&quot;datastructure4_5&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;🎃常数阶&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;for (i = 0; i &amp;lt; 1000; i ++) {
	x ++;
  s = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上，如果算法的执行时间不随问题规模n的增加而增长，算法中语句频度就是某个常数。即使这个常数再大，算法的时间复杂度都是&lt;strong&gt;O(1)&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;🎉线性阶&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;给小灰一个长度为10cm的面包，小灰每三分钟吃掉1cm，那么他吃掉整个面包要多久？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;答案自然是&lt;strong&gt;3*10=30min&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果面包的长度为n cm呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时吃掉整个面包需要3*n即&lt;strong&gt;3n分钟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果用一个函数来表达吃掉整个面包所需要的时间可以记作&lt;strong&gt;T(n)=3n&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(n表示面包的长度即处理的&lt;strong&gt;数据的规模&lt;/strong&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;🎐对数阶&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;给小灰一个长度为16cm的面包，小灰小灰每5min吃掉面包剩余长度的一半，第1min吃掉8cm，第2min吃掉4cm，第三min吃掉2cm......那么小灰把面包吃得只剩下1cm，需要多少天呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题翻译一下，就是数字16不断地除以2，除几次以后的结果等于1？&lt;/p&gt;
&lt;p&gt;这里要涉及到数学当中的对数，以2位底，16的对数，可以简写为&lt;strong&gt;log₂16&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，把面包吃得只剩下1寸，需要 &lt;strong&gt;5*log₂16=5*4=20 min&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果面包的长度为n cm呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时吃掉整个面包需要&lt;strong&gt;5*log₂n&lt;/strong&gt;分钟记作&lt;strong&gt;T(n)=5*log₂n&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🎎平方阶&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_6.png!v1&quot; alt=&quot;datastructure4_6&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于当i=0时内循环执行n次，当i=1时内循环执行n-1次，...，当i=n-1时内循环执行1次总执行次数&lt;/p&gt;
&lt;p&gt;n+(n-1)++(n-2)+...+1=n(n+1)/2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;时间复杂度是&lt;strong&gt;O(n²)&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;🥽立方阶&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_7.png!v1&quot; alt=&quot;datastructure4_7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_8.png!v1&quot; alt=&quot;datastructure4_8&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;🧶最好、最坏和平均时间复杂度&lt;/h4&gt;
&lt;p&gt;有的情况算法的基本操作重复执行的次数还随问题&lt;strong&gt;输入的数据集&lt;/strong&gt;不同而不同&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_9.png!v1&quot; alt=&quot;datastructure4_9&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_10.png!v1&quot; alt=&quot;datastructure4_10&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通常考虑最坏和平均&lt;/p&gt;
&lt;p&gt;但有时平均比较难计算&lt;/p&gt;
&lt;p&gt;只考虑最坏时间复杂度，最坏情况运行时间是一种保证，那就是运行时间不会再坏了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;🎑计算公式&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_11.png!v1&quot; alt=&quot;datastructure4_11&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_12.png!v1&quot; alt=&quot;datastructure4_12&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_13.png!v1&quot; alt=&quot;datastructure4_13&quot; /&gt;&lt;/p&gt;
&lt;p&gt;常见的时间复杂度按数量级递增排列依次为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;常数阶O(1)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对数阶O(log₂n)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线性阶O(n)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线性对数阶O(nlog₂n)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平方阶O(n²)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;立方阶O(n³)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;......&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;k次方阶O(n k次方)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指数阶O(2 n次方)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🎉算法的空间复杂度&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;算法本身要占据的空间：输入/输出、指令、常数、变量等&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;算法要使用的辅助空间&lt;/p&gt;
&lt;p&gt;若输入数据所占据的空间只取决于问题本身和算法无关，这样只需分析该算法在实现时所需的辅助单元即可，若算法执行时所需的辅助单元相对于输入数据量而言是个常数，则称此算法为&lt;strong&gt;原地施工&lt;/strong&gt;，空间复杂度为&lt;strong&gt;O(1)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/datastructure4_14.png!v1&quot; alt=&quot;datastructure4_14&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;绝大多数情况下，时间复杂度更重要一些，我们宁愿多分配一些内存空间也要提升程序的执行速度。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>🎊算法和算法分析</title><link>https://www.corgi37.top/posts/datastructure3/</link><guid isPermaLink="true">https://www.corgi37.top/posts/datastructure3/</guid><pubDate>Mon, 09 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;🐱‍🏍 算法的定义&lt;/h3&gt;
&lt;p&gt;对特定问题&lt;strong&gt;求解方法&lt;/strong&gt;和&lt;strong&gt;步骤&lt;/strong&gt;的一种&lt;strong&gt;描述&lt;/strong&gt;，它是指令的&lt;strong&gt;有限序列&lt;/strong&gt;。其中每个指令表示&lt;strong&gt;一个&lt;/strong&gt;或&lt;strong&gt;多个操作&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;简而言之算法就是&lt;strong&gt;解决问题的方法和步骤。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🎍算法的描述&lt;/h3&gt;
&lt;p&gt;算法可以用以下几种方式描述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自然语言【英文、中文】；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程图【传统流程图、NS流程图、框图】；&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;传统流程图为框图，NS流程图也称为盒图&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/1.png!v1&quot; alt=&quot;1&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;伪代码(类C语言)；&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;类C语言介于伪码语言和程序设计语言之间的一种表示形式，保留了C语言的精华，不拘泥于C语言的语法细节，同时也添加了一些C++的成分。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;特点：便于理解，阅读能方便的转换成C语言。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;程序设计(C、Java...)。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;🎫程序与算法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;程序 = 数据结构 + 算法&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;算法&lt;/strong&gt;是解决问题的一种方法或一个过程，考虑如何将输入转换成输出，一个问题可以有多种算法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;程序&lt;/strong&gt;是某种程序设计语言对算法的具体实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据结构通过算法来实现操作。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;算法根据数据结构设计程序。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;👘算法的特性&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有穷性&lt;/strong&gt;：算法在执行有限步骤之后，自动结束而不会出现无限循环，并且每一个步骤都在可接受的时间范围内完成。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然这里的有穷并不是纯数学意义的，而是在实际应用中合理的、可以接受的“边界”。你说你写一个算法，计算机需要算上20年，一定会结束，他在数学上是有穷的，媳妇都熬成婆了，算法的意义就不大了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;确定性&lt;/strong&gt;：算法的每一个步骤都有确定的含义，不会出现二义性(不会有歧义)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可行性&lt;/strong&gt;：算法是可执行的，算法中的所有操作都可以通过已经实现的基本操作运算执行有限次来实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：一个算法有零个或多个输入。当用函数描述算法时，输入往往是通过形参表示的，在它们被调用时，从主调函数获得输入值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：一个算法有一个或多个输出，它们是算法进行信息加工后得到的结果，无输出的算法没有任何意义。当用函数描述算法时，输出多用返回值或引用类型的形参表示。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;🎍算法设计的要求&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;好的算法应该具有&lt;strong&gt;正确性、可读性、健壮性、时间效率高&lt;/strong&gt;和&lt;strong&gt;存储量低&lt;/strong&gt;的特征。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;正确性(Correctness)&lt;/strong&gt;：能正确的反映问题的需求，能得到正确的答案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分以下四个层次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a.算法程序&lt;strong&gt;没有语法错误&lt;/strong&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;b.算法程序对&lt;strong&gt;n组&lt;/strong&gt;输入产生正确的结果；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;c.算法程序对&lt;strong&gt;典型、苛刻、有刁难性&lt;/strong&gt;的几组输入可以产生正确的结果；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;d.算法程序对&lt;strong&gt;所有输入&lt;/strong&gt;产生正确的结果；&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;但我们不可能逐一的验证所有的输入，因此算法的正确性在大多数情况下都不可能用程序证明，而是用数学方法证明。所以一般情况下我们把&lt;strong&gt;层次3&lt;/strong&gt;作为算法是否正确的标准。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可读性(Readability)&lt;/strong&gt;：算法，首先应便于人们理解和相互交流，其次才是机器可执行性。可读性强的算法有助于人们对算法的理解，而难懂的算法易于隐藏错误，且难于调试和修改。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;健壮性(Robustness)&lt;/strong&gt;：当输入的数据非法时，好的算法能适当地做出正确反应或进行相应处理，而不会产生一些莫名其妙的输出结果。【健壮性又叫又名&lt;strong&gt;鲁棒性,&lt;/strong&gt; 即使用棒子粗鲁地对待他也可以执行, 类似于Java预料到可能出现的异常并对其进行捕获处理】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(&lt;strong&gt;高效性（Efficiency）&lt;/strong&gt;)时间效率高和存储量低&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;🛒算法分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一个 好的算法首先要具备正确性，然后是健壮性，可读性，在几个方面都满足的条件下，主要考虑算法的效率，通过算法的效率高低来评判不同算法的优劣程度。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(时间效率)运行时间的长短和(空间效率)占用内存空间的大小是衡量算法好坏的重要因素。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;时间效率&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;指的是算法所耗费的时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;空间效率&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;指的是算法执行过程中所耗费的存储空间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;时间效率和空间效率有时候是互相矛盾的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;👕算法时间效率的度量&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;算法时间效率可以依据该算法编制的程序在计算机上执行所消耗的时间来度量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;两种度量方法&lt;/h4&gt;
&lt;h5&gt;1. 事后统计&lt;/h5&gt;
&lt;p&gt;将算法实现，测算其时间和空间开销。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;缺点：编写程序实现算法将花费较多的时间和精力；所得的结果依赖于计算机的软硬件等环境因素，掩盖算法其本身的优劣。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;&lt;strong&gt;2&lt;/strong&gt;. &lt;strong&gt;事前分析&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;对算法所消耗资源的一种估算方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通常采用事前分析估算法.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;p&gt;​     一个算法的&lt;strong&gt;运行时间&lt;/strong&gt;是指一个算法在计算机上运行所耗费的时间大致等于计算机执行一种简单的操作（如赋值、比较、移动等）所需要的&lt;strong&gt;时间&lt;/strong&gt;与算法中进行的简单操作的&lt;strong&gt;次数乘积&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;算法运行时间 = 一个简单的操作所需的时间 * 简单的操作次数&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;也即算法中每条语句的执行时间之和&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;大写Σ（Sigma [西格玛]）用于数学上的总和符号，指求和&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;算法运行时间 = Σ每条语句的执行次数 * 该语句执行一次所需时间。&lt;/p&gt;
&lt;p&gt;其中每条语句执行次数也称&lt;strong&gt;语句频度&lt;/strong&gt;，所以公式可以表示为&lt;/p&gt;
&lt;p&gt;&lt;code&gt;算法运行时间 = Σ每条语句频度 * 该语句执行一次所需时间。&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;语句的执行要由源程序经编译程序翻译成目标代码，目标代码经装配再执行，因此语句执行一次实际所需的具体时间是与机器的软、硬件环境（如机器速度、编译程序质量等）密切相关的，它与算法无关。&lt;/p&gt;
&lt;p&gt;所以，我们可以假设&lt;strong&gt;执行每条语句所需的时间&lt;/strong&gt;均为&lt;strong&gt;单位时间&lt;/strong&gt;。此时对算法的运行时间的讨论就可转化为讨论&lt;strong&gt;该算法中所有语句的执行次数&lt;/strong&gt;，即频度之和了。&lt;/p&gt;
&lt;p&gt;例如：两个n * n的矩阵相乘的算法可描述为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/2.png!v1&quot; alt=&quot;2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;i从1~n首先判断条件是否成立，条件满足执行循环体并i++，i=n+1判断条件是否不满足，退出循环，判断n+1次循环体执行了n次&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;我们把算法所耗费的时间定义为该算法中每条语句的频度之和，则上述算法的时间消耗T(n)为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;T(n) = 2n³ + 3n² + 2n + 1&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了便于比较不同算法的时间效率，我们仅比较它们的&lt;strong&gt;数量级&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：两个不同的算法，时间消耗分别是：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;T₁(n) = 10n² 与 T₂(n) = 5n³&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后者不如前者&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>🚀数据结构绪论</title><link>https://www.corgi37.top/posts/datastructure1/</link><guid isPermaLink="true">https://www.corgi37.top/posts/datastructure1/</guid><pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;joe-mtitle title=&quot;程序=数据结构+算法&quot; style=&quot;font-weight: bold;font-size: 16px;&quot;&amp;gt;&amp;lt;/joe-mtitle&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个公式展示了程序的本质&lt;/p&gt;
&lt;p&gt;凭借一句话获得图灵奖的Pascal语言之父——Nicklaus Wirth,让他获得图灵奖的这句话就是他提出的著名公式&lt;/p&gt;
&lt;p&gt;这个公式对计算机科学的影响程度足以类似物理学中爱因斯坦的E=mc^2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;算法&lt;/strong&gt;其实就是用于解决某一类问题的公式与思想（给出问题的数学模型）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而&lt;strong&gt;数据结构&lt;/strong&gt;就是数据的&lt;strong&gt;组织、管理和存储格式&lt;/strong&gt;，其使用目的是为了&lt;strong&gt;高效的访问和修改数据&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;至于&lt;strong&gt;程序&lt;/strong&gt;就是计算机处理问题的一系列&lt;strong&gt;指令&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;程序设计&lt;/strong&gt;的实质是对确定的问题选择一种好的&lt;strong&gt;数据结构&lt;/strong&gt;，并设计一种好的&lt;strong&gt;算法&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;课程内容&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/course_content.png!v1&quot; alt=&quot;课程内容&quot; title=&quot;课程内容&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;数据结构的重要性&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;数据结构是计算机软件相关专业的&lt;strong&gt;专业基础课&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在 教学计划中的地位 ：&lt;strong&gt;承上启下&lt;/strong&gt;、&lt;strong&gt;核心部分&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/sort.png!v1&quot; alt=&quot;sort&quot; title=&quot;数据结构的地位&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;数据结构是介于&lt;strong&gt;数学、计算机硬件&lt;/strong&gt;和&lt;strong&gt;计算机软件&lt;/strong&gt;之间的一门核心课程&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/sort2.png!v1&quot; alt=&quot;sort2&quot; title=&quot;数据结构的关系&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类似于武术中的基本功，练武不练功，到头一场空&lt;/li&gt;
&lt;li&gt;考研必考专业课4门专业课共150分【《数据结构与算法》占45分(更有很多学校只考数据结构与算法)】&lt;/li&gt;
&lt;li&gt;找工作面试，主要考核的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据结构研究的内容&lt;/h2&gt;
&lt;p&gt;起初计算机被人们视作数值计算的工具。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常用计算机解决一个问题的步骤：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;具体问题抽象成数学模型；&lt;/li&gt;
&lt;li&gt;分析问题；&lt;/li&gt;
&lt;li&gt;提取操作对象；&lt;/li&gt;
&lt;li&gt;找出操作对象之间的关系；&lt;/li&gt;
&lt;li&gt;用数学语言描述==&amp;gt;数据结构;【建立相应方程】&amp;lt;一般建立方程容易,数据元素之间的关系简单,但运算量大，人们就利用计算机来快速的完成复杂的计算&amp;gt;&lt;/li&gt;
&lt;li&gt;设计算法；&lt;/li&gt;
&lt;li&gt;编程调试运行&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;随着计算机应用领域的扩展，计算机被越来越多地用于&lt;strong&gt;非数值计算&lt;/strong&gt;，比如&lt;strong&gt;信息的处理&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;线性表（线性关系）举例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/line.png!v1&quot; alt=&quot;line&quot; /&gt;&lt;/p&gt;
&lt;p&gt;来了一位新同学把他的信息加入到系统中；有同学转学或出国了要把他的信息删除；想查看某位同学的信息；修改某位同学改名字了在系统中也应相应的修改。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;操作对象:每位学生的信息(姓名、学号、性别、籍贯、专业)
操作算法：查询、插入、修改、删除等
操作对象之间的关系：线性关系
数据结构：线性数据结构，线性表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;树（非线性关系）举例1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/tree.png!v1&quot; alt=&quot;tree&quot; /&gt;&lt;/p&gt;
&lt;p&gt;计算机之所以能和人对弈是因为已经将对弈的策略在计算机中存储好，由于对弈的过程是在一定规则下随机进行的，所以，为使计算机能灵活对弈，就必须把对弈过程中所有可能发生的情况及相应的对策都加以考虑。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;操作对象：各种棋盘格局
操作算法：根据当前的格局，从提供的派生格局中选择一种。也就是下一步棋，则构成一个新的棋盘格局。
操作对象之间的关系：非线性关系(树）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;树（非线性关系）举例2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/tree2.png!v1&quot; alt=&quot;tree2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;磁盘根目录下有很多子目录及文件，&lt;/p&gt;
&lt;p&gt;每个子目录里又可以包含多个子目录及文件但每一个子目录只有一个父目录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;图（非线性关系）举例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/map.png!v1&quot; alt=&quot;map&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最短路径问题。&lt;/p&gt;
&lt;p&gt;从城市A到城市B有多条线路，但每条线路的交通费不同，&lt;/p&gt;
&lt;p&gt;那么，如何选择一条线路，使得从城市A到城市B的交通费用最少呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;综上所述：这些问题的共性是都无法用数学的公式或方程来描述，是一些“&lt;strong&gt;非数值计算&lt;/strong&gt;”的程序设计问题，描述非数值计算问题的数学模型不是数学方程，而是诸如&lt;strong&gt;表，树和图&lt;/strong&gt;之类的&lt;strong&gt;具有逻辑关系的数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据结构是一门研究&lt;strong&gt;非数值计算&lt;/strong&gt;的程序设计中计算机的&lt;strong&gt;操作对象&lt;/strong&gt;以及它们之间的&lt;strong&gt;关系和操作&lt;/strong&gt;的学科&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线性关系是互不相干的独立关系，而非线性则是相互作用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据结构比较难学。抽象概念性强、需要一些数学逻辑思维，不然后面的学习就吃力一些了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>✨ 基本概念和术语</title><link>https://www.corgi37.top/posts/datastructure2/</link><guid isPermaLink="true">https://www.corgi37.top/posts/datastructure2/</guid><pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;🐱‍🏍 数据结构的基本概念和术语&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据&lt;/strong&gt;(Data)是客观事物的符号表示，是所有能&lt;strong&gt;输入到计算机中&lt;/strong&gt;并被&lt;strong&gt;计算机程序处理的符号&lt;/strong&gt;的总称（&lt;strong&gt;集合&lt;/strong&gt;）。
是&lt;strong&gt;信息的载体&lt;/strong&gt;；是对客观事物的&lt;strong&gt;符号化&lt;/strong&gt;表示；可以被计算机&lt;strong&gt;识别&lt;/strong&gt;、&lt;strong&gt;存储&lt;/strong&gt;和&lt;strong&gt;加工&lt;/strong&gt;。数据不仅仅包含&lt;strong&gt;整型、实型&lt;/strong&gt;等数值类型，还包含&lt;strong&gt;图形、图像、声音、视频及动画&lt;/strong&gt;等&lt;strong&gt;非数值类型。&lt;/strong&gt;
对于&lt;strong&gt;整型、实型&lt;/strong&gt;等数值类型,可以进行&lt;strong&gt;数值计算&lt;/strong&gt;；对于字符数据类型，就需要进行&lt;strong&gt;非数值的处理&lt;/strong&gt;。而声音、图像、视频等其实是可以通过&lt;strong&gt;编码&lt;/strong&gt;的手段变成字符数据来处理的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据元素&lt;/strong&gt;(DataElement)是数据的&lt;strong&gt;基本单位&lt;/strong&gt;，在计算机中通常作为一个&lt;strong&gt;整体&lt;/strong&gt;进行考虑和处理。
在有些情况下，数据元素也称为&lt;strong&gt;元素&lt;/strong&gt;、&lt;strong&gt;记录&lt;/strong&gt;、&lt;strong&gt;节点&lt;/strong&gt;、&lt;strong&gt;顶点&lt;/strong&gt;等。如前一节示例中的一名学生&lt;strong&gt;记录&lt;/strong&gt;，树中棋盘的一个&lt;strong&gt;格局&lt;/strong&gt;（状态），以及图中的一个&lt;strong&gt;顶点&lt;/strong&gt;等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据项&lt;/strong&gt;(Data Item)是&lt;strong&gt;组成数据元素&lt;/strong&gt;的、&lt;strong&gt;有独立含义&lt;/strong&gt;的、&lt;strong&gt;不可分割的最小单位&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/table.png!v1&quot; alt=&quot;table&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，学生基本信息表中的学号、姓名、性别等都是&lt;strong&gt;数据项&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;【数据项是“数据的最小单位。但真正讨论问题时，&lt;strong&gt;数据元素&lt;/strong&gt;才是数据结构中建立数据模型的着眼点。就像我们讨论一部电影时，是讨论这部电影角色这样的&lt;strong&gt;数据元素&lt;/strong&gt;”，而不是针对这个角色的姓名或者年龄这样的“数据项”去研究分析。】&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据对象&lt;/strong&gt;(DataObject)是&lt;strong&gt;性质相同&lt;/strong&gt;的&lt;strong&gt;数据元素的集合&lt;/strong&gt;，是数据的一个&lt;strong&gt;子集&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如：&lt;strong&gt;整数数据对象&lt;/strong&gt;是集合N={0, ±1,±2,...}, &lt;strong&gt;字母字符数据对象&lt;/strong&gt;是集合C={&apos;A&apos;,&apos;B&apos;, ...‘Z’,&apos;a&apos;,&apos;b&apos;, ...， &apos;z&apos;}, &lt;strong&gt;学生基本信息表&lt;/strong&gt;也可以是一个数据对象。&lt;/p&gt;
&lt;p&gt;由此可以看出，不论数据元素集合是无限集（如整数集），或是有限集（如字母字符集），还是由多个数据项组成的&lt;strong&gt;复合数据元素&lt;/strong&gt;（如学生表）的集合，只要集合内元素的&lt;strong&gt;性质&lt;/strong&gt;均相同，都可称之为一个&lt;strong&gt;数据对象&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说了数据结构中的数据那数据结构中的&lt;strong&gt;结构&lt;/strong&gt;又是什么呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🎶 数据的结构&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;结构&lt;/strong&gt;，简单的理解就是&lt;strong&gt;关系&lt;/strong&gt;，比如分子结构，就是说组成分子的原子之间的排列方式。&lt;/p&gt;
&lt;p&gt;严格点说，结构是指&lt;strong&gt;各个组成部分相互搭配和排列的方式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在现实世界中，不同数据元素之间&lt;strong&gt;不是独立的&lt;/strong&gt;，而是存在&lt;strong&gt;特定的关系&lt;/strong&gt;，我们将这些关系称为&lt;strong&gt;结构&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那数据结构是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据结构&lt;/strong&gt;(Data Structure)是相互之间存在&lt;strong&gt;一种或多种特定关系&lt;/strong&gt;的&lt;strong&gt;数据元素&lt;/strong&gt;的&lt;strong&gt;集合&lt;/strong&gt;。换句话说，数据结构是带“&lt;strong&gt;结构&lt;/strong&gt;”的&lt;strong&gt;数据元素的集合&lt;/strong&gt;，“结构”就是指数据元素之间存在的&lt;strong&gt;关系&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🎂数据结构包含的内容&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;数据结构包含以下三个方面的内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;数据元素之间的逻辑关系， 也称为&lt;strong&gt;逻辑结构&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;数据元素及其关系在&lt;strong&gt;内存中的表示&lt;/strong&gt;（又称为&lt;strong&gt;映像&lt;/strong&gt;），称为数据的&lt;strong&gt;物理结构&lt;/strong&gt;或数据的&lt;strong&gt;存储结构&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;数据的&lt;strong&gt;运算和实现&lt;/strong&gt;，即对数据元素可以施加的&lt;strong&gt;操作&lt;/strong&gt;以及这些操作在相应的&lt;strong&gt;存储结构&lt;/strong&gt;上的&lt;strong&gt;实现&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;👔数据结构的两个层次&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;逻辑结构&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;数据的&lt;strong&gt;逻辑结构&lt;/strong&gt;是从&lt;strong&gt;逻辑关系&lt;/strong&gt;上描述数据，&lt;/p&gt;
&lt;p&gt;它与数据的存储无关，是&lt;strong&gt;独立于计算机&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;因此，数据的逻辑结构可以看作是从具体问题抽象出来的&lt;strong&gt;数学模型&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据的逻辑结构有两个要素：一是&lt;strong&gt;数据元素&lt;/strong&gt;；二是&lt;strong&gt;关系&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/2.png!v1&quot; alt=&quot;2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/3.png!v1&quot; alt=&quot;3&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;物理结构（存储结构）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;数据元素及其关系在计算机存储器中的&lt;strong&gt;存储结构&lt;/strong&gt;、存储方式。&lt;/p&gt;
&lt;p&gt;它是数据结构在&lt;strong&gt;计算机中的表示&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑结构与存储结构的关系&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;存储结构&lt;/strong&gt;是&lt;strong&gt;逻辑关系的映像&lt;/strong&gt;与&lt;strong&gt;元素本身的映像。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑结构&lt;/strong&gt;是数据结构的&lt;strong&gt;抽象&lt;/strong&gt;， &lt;strong&gt;存储结构&lt;/strong&gt;是数据结构的&lt;strong&gt;实现&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;两者综合起来建立了数据元素之间的&lt;strong&gt;结构关系&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🎟逻辑结构的种类&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;逻辑结构的种类有两种划分方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;划分方法一：&lt;/h4&gt;
&lt;h5&gt;一、线性结构&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;有且只有一个开始和终端结点，并且所有结点最多只有一个直接前趋和一个直接后继。&lt;/p&gt;
&lt;p&gt;例如：线性表、栈、队列、串&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;二、非线性结构&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;一个结点可能有多个直接前趋和直接后继 例如树、图&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;划分方法二：&lt;/h4&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/4.png!v1&quot; alt=&quot;4&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h5&gt;一、集合结构&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;结构中的数据元素之间除了&lt;strong&gt;同属于一个集合&lt;/strong&gt;的关系外，无任何其他关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;二、线性结构&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;结构中的数据元素之间存在着一对一的线性关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;三、树形结构&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;结构中的数据元素之间存在着一对多的层次关系&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;四、图形结构/网状结构&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;结构中的数据元素之间存在着多对多的任意关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🎏存储结构的种类&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;顺序存储结构&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;顺序存储结构是把数据元素存放在&lt;strong&gt;连续的存储单元&lt;/strong&gt;里，数据元素之间的逻辑关系是通过&lt;strong&gt;数据元素的位置&lt;/strong&gt;。(在前面的数据元素就存在前面；在后面的数据元素就存在后面)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;C语言用数组来实现顺序存储结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/5.png!v1&quot; alt=&quot;5&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;链式存储结构&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;用一组&lt;strong&gt;任意的存储单元&lt;/strong&gt;存储数据元素(可能连续也可能不连续)，数据元素之间的逻辑关系用&lt;strong&gt;指针&lt;/strong&gt;来表示(用指针存放后继元素的&lt;strong&gt;存储地址&lt;/strong&gt;)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;C语言中用&lt;strong&gt;指针&lt;/strong&gt;来实现链式存储结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/6.png!v1&quot; alt=&quot;6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/7.png!v1&quot; alt=&quot;7&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;索引存储结构&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/8.png!v1&quot; alt=&quot;8&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在存储&lt;strong&gt;节点信息&lt;/strong&gt;的同时，还建立&lt;strong&gt;附加索引&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;索引表中的每一项称为一个&lt;strong&gt;索引项&lt;/strong&gt;，索引项的一般形式是：(&lt;strong&gt;关键字&lt;/strong&gt;，&lt;strong&gt;地址&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;关键字是能&lt;strong&gt;唯一标识一个结点&lt;/strong&gt;的那些数据项。&lt;/p&gt;
&lt;p&gt;若&lt;strong&gt;每个结点&lt;/strong&gt;在索引表中都有&lt;strong&gt;一个索引项&lt;/strong&gt;，则该索引表称之为&lt;strong&gt;稠密索引&lt;/strong&gt;（Dense Index)。若&lt;strong&gt;一组结点&lt;/strong&gt;在索引表中&lt;strong&gt;只对应一个索引项&lt;/strong&gt;，则该索引表称之为&lt;strong&gt;稀疏索引&lt;/strong&gt;（Sparse Index)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;散列存储结构&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/9.png!v1&quot; alt=&quot;9&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据结点的&lt;strong&gt;关键字&lt;/strong&gt;直接计算出该结点的&lt;strong&gt;存储地址&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;👕数据类型&lt;/h3&gt;
&lt;p&gt;数据类型是一组&lt;strong&gt;性质相同的值的集合&lt;/strong&gt;以及&lt;strong&gt;定义于这个值集合上的一组操作&lt;/strong&gt;的总称。&lt;/p&gt;
&lt;p&gt;（&lt;strong&gt;数据类型 = 值的集合 + 值集合上的一组操作&lt;/strong&gt; ）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说到数据类型其实我们并不陌生，在使用高级程序设计语言编写程序时，必须对程序中出现的每个变量、常量或表达式、C语言中函数的参数、返回值，明确说明它们所属的&lt;strong&gt;数据类型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;C语言中：&lt;/p&gt;
&lt;p&gt;提供int,char,float,double等基本数据类型；&lt;/p&gt;
&lt;p&gt;数组、结构、共用体、枚举等构造数据类型；&lt;/p&gt;
&lt;p&gt;还有指针、空（void)类型，用户也可用typedef自己定义数据类型。&lt;/p&gt;
&lt;p&gt;而另一些常用的数据结构，如栈、队列、树、图等，不能直接用数据类型来表示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;数据类型明显或隐含地规定了程序执行期间&lt;strong&gt;变量或表达式&lt;/strong&gt;的&lt;strong&gt;取值范围、存储方式&lt;/strong&gt;以及&lt;strong&gt;允许进行的运算&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，C语言中定义变量i为int类型，就表示是[min,max]范围的整数，[-32768～32767,16位]&lt;/p&gt;
&lt;p&gt;在这个整数集上可以进行+、-、*、/、%的操作，而不能进行其他数据类型比如字符串的一些操作，而实型变量也有自己的取值范围和相应运算，比如取模运算是不能用于实型变量的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在C语言中，数据类型可以分为两类：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;原子类型&lt;/strong&gt;：是不可以再分解的&lt;strong&gt;基本类型&lt;/strong&gt;，包括&lt;strong&gt;整型、实型、字符型&lt;/strong&gt;等&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结构类型&lt;/strong&gt;：由若干个类型组合而成，是&lt;strong&gt;可以再分解&lt;/strong&gt;的。&lt;/p&gt;
&lt;h3&gt;🧵抽象数据类型&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/10.png!v1&quot; alt=&quot;10&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/11.png!v1&quot; alt=&quot;11&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你在这幅图中看到了什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;相信大多数人会说看到的圆，其实这就是&lt;strong&gt;抽象&lt;/strong&gt;。我们看到了他的&lt;strong&gt;本质&lt;/strong&gt;，而去掉了一些&lt;strong&gt;非本质&lt;/strong&gt;的东西，比如大小颜色，线条的粗细，空心还是实心。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那什么是圆呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;圆是到某个点距离相等的点的集合这个定点就是圆心，距离就是半径，我们就可以描述这个圆的一些相关信息了&lt;/p&gt;
&lt;p&gt;运算:&lt;strong&gt;构造圆、求面积、求周长&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;抽象数据类型&lt;/strong&gt;(Abstract Data Type, ADT)一般指由&lt;strong&gt;用户定义的&lt;/strong&gt;、&lt;strong&gt;表示应用问题&lt;/strong&gt;的&lt;strong&gt;数学模型&lt;/strong&gt;，以及定义在这个模型上的&lt;strong&gt;一组操作&lt;/strong&gt;的总称。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体包括三部分：数据对象、数据对象上关系的集合以及对数据对象的基本操作的集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;由用户定义，从问题抽象出&lt;strong&gt;数据模型&lt;/strong&gt;（逻辑结构）&lt;/li&gt;
&lt;li&gt;还包括定义在数据模型上的一组&lt;strong&gt;抽象运算&lt;/strong&gt;（相关操作）&lt;/li&gt;
&lt;li&gt;不考虑计算机内的具体存储结构与运算的具体&lt;strong&gt;实现算法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;抽象数据类型的形式化定义&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;抽象对象可用（D, S, P）三元组【离散数学上的概念】表示。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中D是数据对象&lt;/p&gt;
&lt;p&gt;S是D上的关系集 (数据对象之间的关系构成的集合，数据对象与数据对象之间可能有多种关系构成了这个集合)&lt;/p&gt;
&lt;p&gt;P是对D的基本操作集&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/12.png!v1&quot; alt=&quot;12&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;🎨抽象数据的举例说明&lt;/h4&gt;
&lt;p&gt;比如求&lt;strong&gt;圆的面积&lt;/strong&gt;的操作area ( 操作的名字 ) ( r ) ( 操作的参数 )&lt;/p&gt;
&lt;p&gt;对图形进行一个缩放 n 倍 scale ( G ( 被操作的图形 ), n ) 对图形进行缩放，它当然也会返回一个图形 G&apos; = scale ( G, n ) 返回值要赋值给 G  写成 scale（ &amp;amp;G, n ）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本操作定义格式说明：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“参数表”&lt;/strong&gt;：赋值参数，只为操作提供输入值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引用参数&lt;/strong&gt;以 &lt;strong&gt;&quot;&amp;amp;&quot;&lt;/strong&gt; 打头，除可提供输入值外，还将返回&lt;strong&gt;操作结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“初始条件”&lt;/strong&gt; 描述了操作执行之前数据结构和参数应满足的条件，若不满足，则操作失败，并返回相应出错信息。若初始条件为空，则省略。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“操作结果”&lt;/strong&gt; 说明了操作正常完成之后，数据结构的&lt;strong&gt;变化状况&lt;/strong&gt;和应&lt;strong&gt;返回的结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/13.png!v1&quot; alt=&quot;13&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抽象数据类型的概念与&lt;strong&gt;面向对象&lt;/strong&gt;方法的思想是一致的。&lt;/p&gt;
&lt;p&gt;抽象数据类型独立于具体实现，将数据和操作封装在一起，使得用户程序只能通过抽象数据类型定义的某些操作来访问其中的数据，从而实现了&lt;strong&gt;信息隐藏&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🛒小结&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/cardof_blog/data_struct_mean/14.png!v1&quot; alt=&quot;14&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>🪁Joe2.0样式</title><link>https://www.corgi37.top/posts/joe20-style/</link><guid isPermaLink="true">https://www.corgi37.top/posts/joe20-style/</guid><pubDate>Mon, 28 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;emoji表情&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;/s/emoji&quot;&gt;前往emoji页面&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;icon&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://bbchin.com/iconfont-joe2.0/&quot;&gt;前往iconfont&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;主题自身已经引入了部分 iconfont 图标，可以直接用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;span class=&quot;joe_lamp&quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;h2&gt;文本元素&lt;/h2&gt;
&lt;h4&gt;标题&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/videotitle.png!v1&quot; alt=&quot;image-20240628223645931&quot; /&gt;&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;ctrl/command+数字1-6&lt;/code&gt;快捷键设置对应各个级别标题，例如：command+1对应一级标题，以此类推。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;分割线&lt;/h4&gt;
&lt;hr /&gt;
&lt;p&gt;可以在一行中用三个以上的星号、减号、底线来建立一个分隔线, 行内不能有其他东西。你也可以在星号或是减号中间插入空格。下面写法都可以建立分隔线。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* * *
***
*****
- - -
--------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;下划线&lt;/h4&gt;
&lt;p&gt;我是&amp;lt;u&amp;gt;下划线&amp;lt;/u&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;&amp;lt;u&amp;gt;下划线&amp;lt;/u&amp;gt;&lt;/code&gt;或是快捷键&lt;code&gt;command+u&lt;/code&gt;设置下划线&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;加粗&lt;/h4&gt;
&lt;p&gt;我是&lt;strong&gt;加粗&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;**加粗**&lt;/code&gt;或是快捷键&lt;code&gt;command+b&lt;/code&gt;设置下划线&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;倾斜&lt;/h4&gt;
&lt;p&gt;我是&lt;em&gt;倾斜&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;*倾斜*&lt;/code&gt;或是快捷键&lt;code&gt;command+i&lt;/code&gt;设置倾斜&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;删除线&lt;/h4&gt;
&lt;p&gt;我是&lt;s&gt;删除线&lt;/s&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;~~删除线~~&lt;/code&gt;或是快捷键&lt;code&gt;control+shift+~&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;引用&lt;/h4&gt;
&lt;h5&gt;区块引用&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;我是区块引用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;&amp;gt;&lt;/code&gt;引用 或是使用&lt;code&gt;command+option+q&lt;/code&gt;来引用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;嵌套引用&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;我是嵌套引用第一层&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我是嵌套引用第二层&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我是嵌套引用最里面一层&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我是嵌套引用第二层&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我是嵌套引用第一层&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 我是嵌套引用第一层
&amp;gt;
&amp;gt; &amp;gt;我是嵌套引用第二层
&amp;gt; &amp;gt;
&amp;gt; &amp;gt; &amp;gt;我是嵌套引用最里面一层
&amp;gt; &amp;gt;
&amp;gt; &amp;gt;我是嵌套引用第二层
&amp;gt;
&amp;gt; 我是嵌套引用第一层
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;引用其他要素&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;引用的区块内也可以使用其他的 Markdown 语法，包括标题、列表、代码区块等:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; - 入我相思门, 知我相思苦。
&amp;gt;
&amp;gt; - 长相思兮长相忆, 短相思兮无穷极。
&amp;gt;
&amp;gt; - 给出一些例子代码:
&amp;gt; ```markdown
&amp;gt;   代码块
&amp;gt; ```
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;入我相思门, 知我相思苦。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;长相思兮长相忆, 短相思兮无穷极。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;给出一些例子代码:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  代码块
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;超链接&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;Markdown 支持两种形式的链接语法: 行内式和参考式两种形式, 行内式一般使用较多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;行内式&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;[]里写链接文字，()里写链接地址, ()中的 &quot;&quot; 中可以为链接指定title属性, title属性可加可不加。title属性的效果是鼠标悬停在链接上会出现指定的 title文字。&lt;code&gt;[链接文字](链接地址 &quot;链接标题&quot;)&lt;/code&gt; 这样的形式。链接地址与链接标题前有一个空格。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;[纵里寻她千百度](https://www.baidu.com)
[纵里寻她千百度](https://www.baidu.com &quot;我是百度&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.baidu.com&quot;&gt;纵里寻她千百度&lt;/a&gt;
&lt;a href=&quot;https://www.baidu.com&quot;&gt;纵里寻她千百度&lt;/a&gt;&lt;/p&gt;
&lt;h5&gt;自动链接&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;Markdown 支持以比较简短的自动链接形式来处理网址和电子邮件信箱, 只要是用 &amp;lt;&amp;gt; 包起来， Markdown 就会自动把它转成链接。一般网址的链接文字就和链接地址一样, 例如:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;https://www.baidu.com&amp;gt;
&amp;lt;cardof@gmail.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.baidu.com&quot;&gt;https://www.baidu.com&lt;/a&gt;
&lt;a href=&quot;mailto:cardof@gmail.com&quot;&gt;cardof@gmail.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;段落缩进&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;&amp;lt;p class=&quot;indent&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/code&gt;包裹段落将会自动缩进，比如&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;p class=&quot;indent&quot;&amp;gt;你可以一辈子不登山，但是你心中一定要有一座山。它使你总往高处爬，它使你总有个奋斗的方向，它使你任何一刻抬起头，都能看到自己的希望。——刘墉《方向》&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;p class=&quot;indent&quot;&amp;gt;你可以一辈子不登山，但是你心中一定要有一座山。它使你总往高处爬，它使你总有个奋斗的方向，它使你任何一刻抬起头，都能看到自己的希望。——刘墉《方向》&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;居中标题&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;&amp;lt;joe-mtitle title=&quot;我是居中&quot;&amp;gt;&amp;lt;/joe-mtitle&amp;gt;&lt;/code&gt;将会居中，注意：此为&lt;strong&gt;joe2.0&lt;/strong&gt;语法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;joe-mtitle title=&quot;我是居中&quot;&amp;gt;&amp;lt;/joe-mtitle&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h2&gt;代码元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;p class=&quot;indent&quot;&amp;gt;对于程序员来说这个功能是必不可少的, 插入程序代码的方式有两种, 一种是利用缩进(Tab), 另一种是利用 “`” 符号(一般在ESC键下方)包裹代码。&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入多行代码, 可以使用缩进或者 &lt;code&gt;code&lt;/code&gt;, 具体看示例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;行内代码&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;插入行内代码, 即插入一个单词或者一句代码的情况，使用 &lt;code&gt;code&lt;/code&gt; 这样的形式插入。&lt;/p&gt;
&lt;p&gt;如PHP打印堆栈信息 &lt;code&gt;debug_backtrace()&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PHP打印堆栈信息 &lt;code&gt;debug_backtrace()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h4&gt;代码块&lt;/h4&gt;
&lt;h5&gt;缩进式多行代码&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;缩进 4 个空格或是 1 个制表符。&lt;/p&gt;
&lt;p&gt;一个代码区块会一直持续到没有缩进的那一行(或是文件结尾)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	$closure = function () use($name) {
	return $name;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$closure = function () use($name) {
	return $name;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用六个 ` 包裹代码&lt;/h5&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;```php
$closure = function () use($name) {
	return $name;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$closure = function () use($name) {
	return $name;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h2&gt;图片元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;图片的创建方式与超链接相似，并且也有两种写法：行内式和参考式写法。&lt;/p&gt;
&lt;p&gt;语法中图片Alt的意思是：如果图片因为某些原因不能显示, 就用定义的图片Alt文字来代替图片。 图片Title则和链接中的Title一样, 表示鼠标悬停与图片上时出现的文字。 Alt 和 Title 都不是必须的, 可以省略, 但建议写上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;行内式&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;语法：&lt;code&gt;![图片Alt](图片地址 &quot;图片title&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;![rosé](https://oss.corgi37.top/corgi/video/home2.jpg!v1 &quot;blackpink&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/home2.jpg!v1&quot; alt=&quot;rosé&quot; title=&quot;blackpink&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;参考式&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;在文档要插入图片的地方写 &lt;code&gt;![图片Alt][标记]&lt;/code&gt;,&lt;/p&gt;
&lt;p&gt;在文档的最后写上&lt;code&gt; [标记]:图片地址 &quot;Title&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;![哆啦A梦][rose]
[rose]:https://oss.corgi37.top/corgi/video/home41.jpg!v1 &quot;blackpink&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/video/home41.jpg!v1&quot; alt=&quot;哆啦A梦&quot; title=&quot;哆啦A梦&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h2&gt;列表元素&lt;/h2&gt;
&lt;h4&gt;无序列表&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;*/+/-&lt;/code&gt;表示无序列表，比如&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MySQL事务具有以下四个属性（通常称为ACID属性）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;* 原子性（Atomicity）&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;- 一致性（Consistency）&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;+ 隔离性（Isolation）&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-持久性（Durability）&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;MySQL事务具有以下四个属性（通常称为ACID属性）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;原子性（Atomicity）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一致性（Consistency）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隔离性（Isolation）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;持久性（Durability）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果需要在列表内嵌入引用，那就需要缩进，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* 原子性
	&amp;gt; 事务是一个原子操作，它要么全部成功，要么全部失败回滚。如果事务中的任何操作失败，则所有操作都将回滚到之前的状态，以确保数据库中的数据不会被部分更改。
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;原子性&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;事务是一个原子操作，它要么全部成功，要么全部失败回滚。如果事务中的任何操作失败，则所有操作都将回滚到之前的状态，以确保数据库中的数据不会被部分更改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;有序列表&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;有序列表则使用数字接着一个英文句点(.)，例如&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mysql事务隔离级别有哪些？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;read uncommitted（读取未提交）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;read committed（读取已提交）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;repeatable read（可重复读取）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;serializable（可串行化）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;mysql事务隔离级别有哪些？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;read uncommitted（读取未提交）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;read committed（读取已提交）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;repeatable read（可重复读取）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;serializable（可串行化）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;任务列表&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;使用带有 &lt;code&gt;* [ ]&lt;/code&gt; 或 &lt;code&gt;* [x]&lt;/code&gt; (未完成或已完成)项的列表语法撰写一个任务列表, 例如:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;* [ ] 早餐&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;* [x] 工作&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;* [ ] 睡觉&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[ ] 早餐&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[x] 工作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[ ] 睡觉&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h2&gt;表格元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;不管是哪种方式, 第一行为表头, 第二行分隔表头和主体部分, 第三行开始每一行为一个表格行。&lt;/li&gt;
&lt;li&gt;列于列之间用管道符|隔开。原生方式的表格每一行的两边也要有管道符。&lt;/li&gt;
&lt;li&gt;第二行可以为当前列指定对齐方向。默认为左对齐, 在-右边加上:就右对齐，居中就两边都加上:。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;简单方式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt; 表头 1 | 表头 2 | 表头 3 
 :- | :-: | -: 
 内容 1 | 内容 2 | 内容 3 
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表头 1&lt;/th&gt;
&lt;th&gt;表头 2&lt;/th&gt;
&lt;th&gt;表头 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;内容 1&lt;/td&gt;
&lt;td&gt;内容 2&lt;/td&gt;
&lt;td&gt;内容 3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;原生方式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;| 表头 1 | 表头 2 | 表头 3 |
| :- | :-: | -: |
| 内容 1 | 内容 2 | 内容 3 |
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表头 1&lt;/th&gt;
&lt;th&gt;表头 2&lt;/th&gt;
&lt;th&gt;表头 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;内容 1&lt;/td&gt;
&lt;td&gt;内容 2&lt;/td&gt;
&lt;td&gt;内容 3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;HTML原始代码&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;在代码区块里面, &amp;amp; 、 &amp;lt; 和 &amp;gt; 会自动转成 HTML 实体, 这样的方式让你非常容易使用 Markdown 插入范例用的 HTML 原始码, 只需要复制贴上, 剩下的 Markdown 都会帮你处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table&amp;gt;
    &amp;lt;tr&amp;gt;
        &amp;lt;th rowspan=&quot;2&quot;&amp;gt;值班人员&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;星期一&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;星期二&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;星期三&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;李强&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;张明&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;王平&amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;th rowspan=&quot;2&quot;&amp;gt;值班人员&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;星期一&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;星期二&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;星期三&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;李强&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;张明&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;王平&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;joe-dotted&amp;gt;&amp;lt;/joe-dotted&amp;gt;&lt;/p&gt;
&lt;h2&gt;图元素&lt;/h2&gt;
&lt;h4&gt;流程图&lt;/h4&gt;
&lt;p&gt;在Markdown中，当你看到代码块被标记为&lt;code&gt;flow&lt;/code&gt;或者&lt;code&gt;mermaid&lt;/code&gt;，这通常意味着这些代码块用于生成流程图或其他图形表示。这两者都是图形化语言，用于在Markdown文档中嵌入并可视化复杂信息。尽管它们的目的相似，但它们在语法和支持的图形类型上有所不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h5&gt;flowchart.js&lt;/h5&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/adrai/flowchart.js&quot;&gt;文档地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flow&lt;/strong&gt; 专门用于生成流程图。与Mermaid相比，Flow可能在某些实现中功能更为专一，主要聚焦于流程图的生成。&lt;/p&gt;
&lt;p&gt;一个使用Flow的流程图大致分为两段, 第一段是定义元素, 第二段是定义元素之间的走向。&lt;/p&gt;
&lt;p&gt;定义元素的语法 &lt;code&gt;tag=&amp;gt;type: content:&amp;gt;url&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;tag就是元素的名字，type是这个元素的类型, 有6中类型,分别为:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;start&lt;/td&gt;
&lt;td&gt;开始&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;end&lt;/td&gt;
&lt;td&gt;结束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;operation&lt;/td&gt;
&lt;td&gt;操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;subroutine&lt;/td&gt;
&lt;td&gt;子程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;condition&lt;/td&gt;
&lt;td&gt;条件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inputoutput&lt;/td&gt;
&lt;td&gt;输入或产出&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;content 就是在框框中要写的内容, 注意type后的冒号与文本之间一定要有个空格。
用-&amp;gt;来连接两个元素, 需要注意的是condition类型, 因为他有yes和no两个分支, 所以要写成:&lt;/p&gt;
&lt;p&gt;c2(yes)-&amp;gt;io-&amp;gt;e
c2(no)-&amp;gt;op2-&amp;gt;e&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;```flow
st=&amp;gt;start: Start:&amp;gt;https://www.markdown-syntax.com
io=&amp;gt;inputoutput: verification
op=&amp;gt;operation: Your Operation
cond=&amp;gt;condition: Yes or No?
sub=&amp;gt;subroutine: Your Subroutine
e=&amp;gt;end
st-&amp;gt;io-&amp;gt;op-&amp;gt;cond
cond(yes)-&amp;gt;e
cond(no)-&amp;gt;sub-&amp;gt;io
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/videoSnipaste_2024-10-29_23-57-04.png!v1&quot; alt=&quot;流程图&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h5&gt;mermaid.js&lt;/h5&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mermaid-js/mermaid&quot;&gt;文档地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mermaid&lt;/strong&gt; 是一个流行的图表工具，它允许通过简洁的文本描述来生成图表和图形。Mermaid支持多种类型的图表，包括但不限于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;流程图&lt;/li&gt;
&lt;li&gt;序列图&lt;/li&gt;
&lt;li&gt;类图&lt;/li&gt;
&lt;li&gt;状态图&lt;/li&gt;
&lt;li&gt;甘特图&lt;/li&gt;
&lt;li&gt;饼图&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;```mermaid
	flowchart LR
    A --x B
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oss.corgi37.top/corgi/videoSnipaste_2024-10-29_23-54-44.png!v1&quot; alt=&quot;流程图&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;区别&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能范围&lt;/strong&gt;：Mermaid提供了更广泛的图表类型支持，而Flow通常专注于流程图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;普及程度&lt;/strong&gt;：Mermaid因其广泛的功能和灵活性，被广泛应用于各种Markdown编辑器和文档系统中。Flow可能在某些特定的环境或工具中使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语法&lt;/strong&gt;：虽然两者都使用类似的简洁文本语法，但具体的语法结构和关键字可能有所不同&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选择使用&lt;strong&gt;Mermaid&lt;/strong&gt;还是&lt;strong&gt;Flow&lt;/strong&gt;主要取决于你的具体需求和你所使用的平台或工具对这些语言的支持程度。如果你需要创建多种类型的图表，Mermaid可能是更好的选择。如果你只需要创建标准的流程图，并且你的工具链支持Flow，那么使用Flow可能更为直接和简单。&lt;/p&gt;
</content:encoded></item></channel></rss>