<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://izualzhy.cn/feed.xml" rel="self" type="application/atom+xml" /><link href="https://izualzhy.cn/" rel="alternate" type="text/html" /><updated>2026-03-01T13:16:03+00:00</updated><id>https://izualzhy.cn/feed.xml</id><title type="html">Ying</title><subtitle>New</subtitle><author><name>ying</name></author><entry><title type="html">宠辱两相授-读《苏东坡新传》-2</title><link href="https://izualzhy.cn/sdpxz-reading-b" rel="alternate" type="text/html" title="宠辱两相授-读《苏东坡新传》-2" /><published>2026-03-01T11:16:31+00:00</published><updated>2026-03-01T11:16:31+00:00</updated><id>https://izualzhy.cn/sdpxz-reading-b</id><content type="html" xml:base="https://izualzhy.cn/sdpxz-reading-b"><![CDATA[<p>此为：黄楼、乌台诗狱、黄州五年、飘泊江淮、风雨京华，共五章的笔记。</p>

<h2 id="1-书中概要">1. 书中概要</h2>

<p><strong>黄楼</strong>是苏轼在徐州任职时抢救水灾后所建：</p>
<blockquote>
  <p>新建大楼，垩以黄土，名之曰“黄楼”，取五行中土能克水之意。</p>
</blockquote>

<p><strong>乌台诗狱</strong>为党争所致，目标是弹劾苏轼。之后苏轼被流放<strong>黄州五年</strong>，宋神宗本不欲治罪，加上太皇太后信任苏轼，逐步启用，于是又有了<strong>漂泊江淮</strong>、<strong>风雨京华</strong>。</p>

<h2 id="2-苏轼的宠与辱">2. 苏轼的宠与辱</h2>

<p>如果说后来的苏轼，做到了宠辱两不惊，那这几章，则是修炼的过程。不读此书，只知他的洒脱自然，却不知他曾受的煎熬。</p>

<p>苏轼任徐州知州后，在一线工作，看到了新法带来的种种好处与坏处，反而更加懂了王安石。也就有了后来司马光全面废黜新法，苏轼力阻，以及跟王荆公相逢泯恩仇，苏轼说“从公已觉十年迟”。</p>

<p>乌台诗狱中，苏轼于八月十八日入狱，至十二月二十九日出狱，历时四个月又十二日。期间惶恐万分，不知该说什么该做什么，连穿什么衣服都要问人，押送路上也试过自尽，完全是正常人的反应，作者也都如实记录。</p>

<p>如果此时的苏轼，就能做到宠辱不惊、大义凛然，反而觉得有些突兀。</p>

<p>黄州五年，苏轼生活艰难。他盘了一块乡野之地，命名为“东坡”，自称“东坡居士”。这个“辱”的过程，得以远离京都，不再有政治抱负。反而成就了苏东坡，成就了一代文学宗师。</p>

<p>这期间他写的诗词，逐渐有了哲学的意味：</p>

<p>他在《念奴娇·赤壁怀古》里写“人生如梦，一尊还酹江月”；在《赤壁赋》里写“盖将自其变者而观之，则天地曾不能以一瞬；自其不变者而观之，则物与我皆无尽也”。</p>

<p>苏轼说要把书读薄：</p>
<blockquote>
  <p>轼答，“我读《汉书》，至今已经抄过三遍。第一次每段事抄三字为题，第二次两字为题，现在只用一字。”</p>
</blockquote>

<p>我觉得如何把一本厚书读薄，用简洁的文字总结，很难。但是现在这些似乎都可以丢给AI去做，而且做的很好，真是头疼。</p>

<p>这几年，苏轼把哲学、美食的技能树都点上了。</p>

<p>之后离开黄州、离开东坡，游庐山，他写到：</p>

<blockquote>
  <p>横看成岭侧成峰，远近高低各不同。不识庐山真面目，只缘身在此山中。</p>
</blockquote>

<p>王国维《人间词话》说：“诗人对宇宙人生，须入乎其内，又须出乎其外。入乎其内故能写之，出乎其外故能观之。”。其实除了诗人，我们做事也应当如此，过程中责无旁贷全力以赴，复盘时又能更上一层楼，以更高的视角总结、观察。</p>

<p>之后重回京师，苏轼再次陷入党争，一心报知遇之恩，但是心直口快又口无遮拦，身心俱疲。得亏有当时的太皇太后保着，才得以全身而退。</p>

<p>这期间，苏轼诗词几乎停更。</p>

<h2 id="3-宋朝的臣子">3. 宋朝的臣子</h2>

<p>读《苏东坡新传》，夹带着多少理解了一点宋朝的历史。其时宋朝国力衰弱，宋神宗力求强大国力，奈何臣子们装糊涂的装糊涂、真傻的真傻。反倒是王安石颇有一些孤勇者的意味，以至于作者提到苏轼后来觉得：</p>

<blockquote>
  <p>检点变法之初，在京时所作言论，大都出于狂热的意气，缺乏冷静思考，也有很多不尽合理之处，至安石已去，反而觉得今日朝堂中，就缺乏像他这么一个敢作敢为的勇者，痛自检点，颇有悔意。
不过争论当时，出言落笔，太过意气用事，却是事实。苏轼今自回顾，当年如有一方面能不那么偏执，依神宗目前之能从善如流，情形绝不会如现在这样糟糕。</p>
</blockquote>

<p>如果多些行动派，少些想借机敛财、上位的庸人，或许王安石变法有那么一丝希望。</p>

<p>书里的臣子，大致分为三类：实干派(但是也是政治家)、理学、文学家。苏轼属于文学一派。</p>]]></content><author><name>ying</name></author><category term="read" /><summary type="html"><![CDATA[此为：黄楼、乌台诗狱、黄州五年、飘泊江淮、风雨京华，共五章的笔记。 1. 书中概要 黄楼是苏轼在徐州任职时抢救水灾后所建： 新建大楼，垩以黄土，名之曰“黄楼”，取五行中土能克水之意。 乌台诗狱为党争所致，目标是弹劾苏轼。之后苏轼被流放黄州五年，宋神宗本不欲治罪，加上太皇太后信任苏轼，逐步启用，于是又有了漂泊江淮、风雨京华。 2. 苏轼的宠与辱 如果说后来的苏轼，做到了宠辱两不惊，那这几章，则是修炼的过程。不读此书，只知他的洒脱自然，却不知他曾受的煎熬。 苏轼任徐州知州后，在一线工作，看到了新法带来的种种好处与坏处，反而更加懂了王安石。也就有了后来司马光全面废黜新法，苏轼力阻，以及跟王荆公相逢泯恩仇，苏轼说“从公已觉十年迟”。 乌台诗狱中，苏轼于八月十八日入狱，至十二月二十九日出狱，历时四个月又十二日。期间惶恐万分，不知该说什么该做什么，连穿什么衣服都要问人，押送路上也试过自尽，完全是正常人的反应，作者也都如实记录。 如果此时的苏轼，就能做到宠辱不惊、大义凛然，反而觉得有些突兀。 黄州五年，苏轼生活艰难。他盘了一块乡野之地，命名为“东坡”，自称“东坡居士”。这个“辱”的过程，得以远离京都，不再有政治抱负。反而成就了苏东坡，成就了一代文学宗师。 这期间他写的诗词，逐渐有了哲学的意味： 他在《念奴娇·赤壁怀古》里写“人生如梦，一尊还酹江月”；在《赤壁赋》里写“盖将自其变者而观之，则天地曾不能以一瞬；自其不变者而观之，则物与我皆无尽也”。 苏轼说要把书读薄： 轼答，“我读《汉书》，至今已经抄过三遍。第一次每段事抄三字为题，第二次两字为题，现在只用一字。” 我觉得如何把一本厚书读薄，用简洁的文字总结，很难。但是现在这些似乎都可以丢给AI去做，而且做的很好，真是头疼。 这几年，苏轼把哲学、美食的技能树都点上了。 之后离开黄州、离开东坡，游庐山，他写到： 横看成岭侧成峰，远近高低各不同。不识庐山真面目，只缘身在此山中。 王国维《人间词话》说：“诗人对宇宙人生，须入乎其内，又须出乎其外。入乎其内故能写之，出乎其外故能观之。”。其实除了诗人，我们做事也应当如此，过程中责无旁贷全力以赴，复盘时又能更上一层楼，以更高的视角总结、观察。 之后重回京师，苏轼再次陷入党争，一心报知遇之恩，但是心直口快又口无遮拦，身心俱疲。得亏有当时的太皇太后保着，才得以全身而退。 这期间，苏轼诗词几乎停更。 3. 宋朝的臣子 读《苏东坡新传》，夹带着多少理解了一点宋朝的历史。其时宋朝国力衰弱，宋神宗力求强大国力，奈何臣子们装糊涂的装糊涂、真傻的真傻。反倒是王安石颇有一些孤勇者的意味，以至于作者提到苏轼后来觉得： 检点变法之初，在京时所作言论，大都出于狂热的意气，缺乏冷静思考，也有很多不尽合理之处，至安石已去，反而觉得今日朝堂中，就缺乏像他这么一个敢作敢为的勇者，痛自检点，颇有悔意。 不过争论当时，出言落笔，太过意气用事，却是事实。苏轼今自回顾，当年如有一方面能不那么偏执，依神宗目前之能从善如流，情形绝不会如现在这样糟糕。 如果多些行动派，少些想借机敛财、上位的庸人，或许王安石变法有那么一丝希望。 书里的臣子，大致分为三类：实干派(但是也是政治家)、理学、文学家。苏轼属于文学一派。]]></summary></entry><entry><title type="html">在名气与牢骚中成长-读《苏东坡新传》-1</title><link href="https://izualzhy.cn/sdpxz-reading-a" rel="alternate" type="text/html" title="在名气与牢骚中成长-读《苏东坡新传》-1" /><published>2026-02-22T20:50:16+00:00</published><updated>2026-02-22T20:50:16+00:00</updated><id>https://izualzhy.cn/sdpxz-reading-a</id><content type="html" xml:base="https://izualzhy.cn/sdpxz-reading-a"><![CDATA[<p>《苏东坡新传》这本书很厚，我读书也慢，为了防止看到后面忘记前面的，因此读几章便做个总结，此为前三章：食蓼少年、变法与党争、马入尘埃。</p>

<h2 id="1-书中概要">1. 书中概要</h2>

<p>东坡诗：“少年辛苦真食蓼，老境清闲如啖蔗。”</p>

<p>这也是第一章题目的由来，苏轼说老年清闲，更多的是人生态度，而不是在仕途成就一番事业后的闲适感。他少年才华横溢，到了京师受人推崇，考试中更是一举成名天下知。这样的经历，已是传奇，此时的苏轼，踌躇满志，充满了古代士人以天下为己任的抱负。</p>

<p>然而很快卷入了变法与党争，由于政见不合，远离京师。</p>

<p>“西湖三载与君同，马入尘埃鹤入笼。”诗人高兴时，写“春风得意马蹄疾”，诗人郁闷时，写“万马齐喑究可哀”。苏轼则写的是分别之情与无奈之感。第三章，他想以千里马入世，却只能马入尘埃。</p>

<h2 id="2-在牢骚中成长">2. 在牢骚中成长</h2>

<p>读完第三章，我心目里不再是那个充满了传奇、高高在上的苏轼，而是一个真实的、不断在成长的苏轼，所以这一节，更想说说苏轼在困境中的一个极大的优点：在牢骚中成长。</p>

<p>苏轼年轻时就写道：“人能碎千金之璧，不能无失声于破釜；能搏猛虎，不能无变色于蜂虿”，说明早早意识到了个人的品质，例如专注的重要性，以及人性里的矛盾。</p>

<p>但是在步入社会这所大学后，关于人性、关于挫折，苏轼还要学习很多。</p>

<p>没人能天生豁达，正是这些经历，促使人思考与成长，才有了我们所熟知的那个苏东坡。有时候你会感慨原来苏轼也是一个普通人，有那么多的矛盾与纠结，这反而是我喜欢本书的地方。</p>

<p>书里说他到了凤翔，心神俱疲：</p>

<blockquote>
  <p>自来凤翔，他对于这么许多牵连不断的吏事，厌倦不堪，以为除了浪费生命之外，身名两皆无益。案牍劳形，问囚理讼，不知所为何来，从前所学，完全抛弃，而一官在身，却又不得不奔走劳役，弄得心神俱疲。</p>
</blockquote>

<p>上级（韩琦）有了命令，他虽不想但是也不得不执行：</p>

<blockquote>
  <p>诏下之日，苏轼便须巡回所属各县，亲自提举这件“刺勇”的大事，而他所亲眼目睹的，则是“愁怨之民，哭声振动”。但这是诏令，地方官责在奉命行事，一点办法也没有。</p>
</blockquote>

<p>当然韩琦也是无奈之举。</p>

<p>到了密州，我从书中读出来的，是一种苏轼极度不想在一线基层干活的心情：</p>

<blockquote>
  <p>苏轼连日尽在田野间察看飞蝗的来势，检查受灾的情况；晚上又须与有关人员研讨捕蝗的方法，劳累不堪。一处事定，又须再去一地，这种单调的胥吏工作，更使他心里充满委屈的感觉。
慨然有被人当作厮役差遣之耻，气起来就想毁车杀马，扯碎衣冠，逃归乡里去读书</p>
</blockquote>

<p>可是在乡里读书之后呢？还是要出来做官。所以读书好与做官好不能完全对等，只能说相比其他途径，书中的道理要来的快、来得多，但是想到了并不代表做到。人教人,教不会;事教人,一次就会。</p>

<p>这点太真实了，任苏轼才华横溢，仕途仍要从基层做起，不像王安石得神宗赏识，令行天下。王安石得到的信任与权利，应该当时很多官员极其羡慕。</p>

<p>但苏轼也真实诠释了，可以发牢骚，但不能真的菜，工作还是要认真干，惠及一方百姓。</p>

<p>或许是先天的乐观、或许是后天的影响，苏轼总能在所在的地方留下政绩与诗句。西湖有苏堤，有望湖楼下水如天，密州有老夫聊发少年狂，有生死两茫茫。</p>

<p>当透过光环看这位千古传奇人物，不再是下笔千言倚马可待，也不再是酒气豪情与月光。看年轻的苏轼，已隐然形成了一种乐观心态，这点非常值得学习。</p>

<p>不过相比苏轼，我感觉苏辙、王安石更加实干一些。</p>]]></content><author><name>ying</name></author><category term="read" /><summary type="html"><![CDATA[《苏东坡新传》这本书很厚，我读书也慢，为了防止看到后面忘记前面的，因此读几章便做个总结，此为前三章：食蓼少年、变法与党争、马入尘埃。 1. 书中概要 东坡诗：“少年辛苦真食蓼，老境清闲如啖蔗。” 这也是第一章题目的由来，苏轼说老年清闲，更多的是人生态度，而不是在仕途成就一番事业后的闲适感。他少年才华横溢，到了京师受人推崇，考试中更是一举成名天下知。这样的经历，已是传奇，此时的苏轼，踌躇满志，充满了古代士人以天下为己任的抱负。 然而很快卷入了变法与党争，由于政见不合，远离京师。 “西湖三载与君同，马入尘埃鹤入笼。”诗人高兴时，写“春风得意马蹄疾”，诗人郁闷时，写“万马齐喑究可哀”。苏轼则写的是分别之情与无奈之感。第三章，他想以千里马入世，却只能马入尘埃。 2. 在牢骚中成长 读完第三章，我心目里不再是那个充满了传奇、高高在上的苏轼，而是一个真实的、不断在成长的苏轼，所以这一节，更想说说苏轼在困境中的一个极大的优点：在牢骚中成长。 苏轼年轻时就写道：“人能碎千金之璧，不能无失声于破釜；能搏猛虎，不能无变色于蜂虿”，说明早早意识到了个人的品质，例如专注的重要性，以及人性里的矛盾。 但是在步入社会这所大学后，关于人性、关于挫折，苏轼还要学习很多。 没人能天生豁达，正是这些经历，促使人思考与成长，才有了我们所熟知的那个苏东坡。有时候你会感慨原来苏轼也是一个普通人，有那么多的矛盾与纠结，这反而是我喜欢本书的地方。 书里说他到了凤翔，心神俱疲： 自来凤翔，他对于这么许多牵连不断的吏事，厌倦不堪，以为除了浪费生命之外，身名两皆无益。案牍劳形，问囚理讼，不知所为何来，从前所学，完全抛弃，而一官在身，却又不得不奔走劳役，弄得心神俱疲。 上级（韩琦）有了命令，他虽不想但是也不得不执行： 诏下之日，苏轼便须巡回所属各县，亲自提举这件“刺勇”的大事，而他所亲眼目睹的，则是“愁怨之民，哭声振动”。但这是诏令，地方官责在奉命行事，一点办法也没有。 当然韩琦也是无奈之举。 到了密州，我从书中读出来的，是一种苏轼极度不想在一线基层干活的心情： 苏轼连日尽在田野间察看飞蝗的来势，检查受灾的情况；晚上又须与有关人员研讨捕蝗的方法，劳累不堪。一处事定，又须再去一地，这种单调的胥吏工作，更使他心里充满委屈的感觉。 慨然有被人当作厮役差遣之耻，气起来就想毁车杀马，扯碎衣冠，逃归乡里去读书 可是在乡里读书之后呢？还是要出来做官。所以读书好与做官好不能完全对等，只能说相比其他途径，书中的道理要来的快、来得多，但是想到了并不代表做到。人教人,教不会;事教人,一次就会。 这点太真实了，任苏轼才华横溢，仕途仍要从基层做起，不像王安石得神宗赏识，令行天下。王安石得到的信任与权利，应该当时很多官员极其羡慕。 但苏轼也真实诠释了，可以发牢骚，但不能真的菜，工作还是要认真干，惠及一方百姓。 或许是先天的乐观、或许是后天的影响，苏轼总能在所在的地方留下政绩与诗句。西湖有苏堤，有望湖楼下水如天，密州有老夫聊发少年狂，有生死两茫茫。 当透过光环看这位千古传奇人物，不再是下笔千言倚马可待，也不再是酒气豪情与月光。看年轻的苏轼，已隐然形成了一种乐观心态，这点非常值得学习。 不过相比苏轼，我感觉苏辙、王安石更加实干一些。]]></summary></entry><entry><title type="html">任务队列 Celery 实践</title><link href="https://izualzhy.cn/celery-practice" rel="alternate" type="text/html" title="任务队列 Celery 实践" /><published>2026-01-11T11:09:51+00:00</published><updated>2026-01-11T11:09:51+00:00</updated><id>https://izualzhy.cn/celery-practice</id><content type="html" xml:base="https://izualzhy.cn/celery-practice"><![CDATA[<p><a href="https://izualzhy.cn/celery-arch">上一篇</a>介绍了 Celery 架构，这篇我们实战看看。</p>

<h2 id="1-例子">1. 例子</h2>

<p>文章从基础、重试、ack、周期任务等几个方面说明，完整的运行例子放在了 <a href="https://github.com/izualzhy/AI-Systems/tree/main/py3_journey/demo_celery">github</a> 上</p>

<h3 id="11-hello-world">1.1. Hello World</h3>

<p>入门例子使用非常简洁，分为两步：</p>
<ol>
  <li>启动Worker: 从 Redis 读取任务，执行<code class="language-plaintext highlighter-rouge">add</code>方法，将结果写回 Redis</li>
  <li><code class="language-plaintext highlighter-rouge">run_simple_task -&gt; add.delay</code>: 写入任务，通过<code class="language-plaintext highlighter-rouge">app</code>分发到 default 队列(存储到 Redis)，并等待读取结果</li>
</ol>

<h4 id="111-消费者-worker">1.1.1. 消费者-Worker</h4>

<p>先从启动 Worker 开始，定义<code class="language-plaintext highlighter-rouge">app/celery_app.py</code>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python
# coding=utf-8
</span>
<span class="kn">from</span> <span class="nn">celery</span> <span class="kn">import</span> <span class="n">Celery</span>
<span class="kn">from</span> <span class="nn">kombu</span> <span class="kn">import</span> <span class="n">Queue</span><span class="p">,</span> <span class="n">Exchange</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">Celery</span><span class="p">(</span><span class="s">'demo_celery'</span><span class="p">,</span>
             <span class="n">broker</span><span class="o">=</span><span class="s">'redis://localhost/0'</span><span class="p">,</span>
             <span class="n">backend</span><span class="o">=</span><span class="s">'redis://localhost/0'</span><span class="p">,</span>
             <span class="n">include</span><span class="o">=</span><span class="p">[</span><span class="s">'app.tasks'</span><span class="p">])</span>
</code></pre></div></div>

<p>指定<code class="language-plaintext highlighter-rouge">app.celery_app</code>，启动 Worker ：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╰─<span class="nv">$ </span><span class="nb">export </span><span class="nv">PYTHONPATH</span><span class="o">=</span><span class="s2">"../..:</span><span class="nv">$PYTHONPATH</span><span class="s2">"</span>
../../.venv/bin/celery <span class="nt">-A</span> app.celery_app worker <span class="nt">-l</span> info <span class="nt">-P</span> threads <span class="nt">-Q</span> default,periodic_tasks <span class="nt">-c</span> 1


celery@yingzdeMacBook-Pro.local v5.5.3 <span class="o">(</span>immunity<span class="o">)</span>

macOS-10.16-x86_64-i386-64bit 2026-01-11 19:08:19

<span class="o">[</span>config]
.&gt; app:         demo_celery:0x10741d9a0
.&gt; transport:   redis://localhost:6379/0
.&gt; results:     redis://localhost/0
.&gt; concurrency: 1 <span class="o">(</span>thread<span class="o">)</span>
.&gt; task events: OFF <span class="o">(</span><span class="nb">enable</span> <span class="nt">-E</span> to monitor tasks <span class="k">in </span>this worker<span class="o">)</span>

<span class="o">[</span>queues]
.&gt; default          <span class="nv">exchange</span><span class="o">=</span>default<span class="o">(</span>direct<span class="o">)</span> <span class="nv">key</span><span class="o">=</span>default
.&gt; periodic_tasks   <span class="nv">exchange</span><span class="o">=</span>periodic_tasks<span class="o">(</span>direct<span class="o">)</span> <span class="nv">key</span><span class="o">=</span>periodic_tasks

<span class="o">[</span>tasks]
  <span class="nb">.</span> app.tasks.ack_task
  <span class="nb">.</span> app.tasks.add
  <span class="nb">.</span> app.tasks.bind_task
  <span class="nb">.</span> app.tasks.failing_task
  <span class="nb">.</span> app.tasks.log_message
  <span class="nb">.</span> app.tasks.no_ack_task
  <span class="nb">.</span> app.tasks.periodic_task
  <span class="nb">.</span> app.tasks.sqrt_task

<span class="o">[</span>2026-01-11 19:08:19,574: INFO/MainProcess] Connected to redis://localhost:6379/0
<span class="o">[</span>2026-01-11 19:08:19,577: INFO/MainProcess] mingle: searching <span class="k">for </span>neighbors
<span class="o">[</span>2026-01-11 19:08:20,582: INFO/MainProcess] mingle: all alone
<span class="o">[</span>2026-01-11 19:08:20,597: INFO/MainProcess] celery@yingzdeMacBook-Pro.local ready.
</code></pre></div></div>

<h4 id="112-生产者">1.1.2. 生产者</h4>

<p>首先需要定义任务本身，写法跟普通函数一样，唯一的区别在于装饰器里指定了 Celery 对象名，即上一节定义的<code class="language-plaintext highlighter-rouge">app = Celery(...)</code>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">py3_journey.demo_celery.app.celery_app</span> <span class="kn">import</span> <span class="n">app</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">task</span>
<span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span>
    <span class="n">task_logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">"Adding </span><span class="si">{</span><span class="n">x</span><span class="si">}</span><span class="s"> and </span><span class="si">{</span><span class="n">y</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span>
</code></pre></div></div>

<p>然后调用<code class="language-plaintext highlighter-rouge">add.delay</code>方法</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">run_simple_task</span><span class="p">():</span>
    <span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"Dispatching simple 'add' task..."</span><span class="p">)</span>
    <span class="c1"># using delay to trigger a task
</span>    <span class="n">result</span> <span class="o">=</span> <span class="n">add</span><span class="p">.</span><span class="n">delay</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">"Task dispatched. Task ID: </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="nb">id</span><span class="si">}</span><span class="s">. Waiting for result..."</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">"Result: </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>执行<code class="language-plaintext highlighter-rouge">run_simple_task</code>就可以看到 Worker 的输出了：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[2026-01-11 21:17:55,407: INFO/MainProcess] Task app.tasks.add[4ae42872-1285-4599-b23a-f4f29f6dd7f9] received
2026-01-11 21:18:45,411 - celery.tasks - [ThreadPoolExecutor-0_0] - INFO - Adding 4 and 4
[2026-01-11 21:18:45,411: INFO/MainProcess] Adding 4 and 4
[2026-01-11 21:18:45,474: INFO/MainProcess] Task app.tasks.add[4ae42872-1285-4599-b23a-f4f29f6dd7f9] succeeded in 50.06529796200084s: 8
</code></pre></div></div>

<p>Worker 执行完成后，也就看到了 Result 的日志：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026-01-11 21:17:55,275 - Dispatching simple 'add' task...
2026-01-11 21:17:55,407 - Task dispatched. Task ID: 4ae42872-1285-4599-b23a-f4f29f6dd7f9. Waiting for result...
2026-01-11 21:18:45,474 - Result: 8
</code></pre></div></div>

<p>从之前的分析知道<code class="language-plaintext highlighter-rouge">add</code>是在 Worker 执行的，所以<strong>如果修改了<code class="language-plaintext highlighter-rouge">add</code>这类执行代码，需要重启/热加载 Worker</strong>。</p>

<p>提交任务的方式有多种：</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">app.send_task</code>: 任务名作为字符串形式，好处是解耦，不需要在一个代码库 ，缺点则是 task 改名、找到谁在调用等运维问题</li>
  <li><code class="language-plaintext highlighter-rouge">task.apply_async()</code>: 标准方式，有 task 对象；</li>
  <li><code class="language-plaintext highlighter-rouge">task.delay()</code>: apply_async 的简化版，简洁、不能指定复杂参数</li>
  <li><code class="language-plaintext highlighter-rouge">task.apply</code>: 在当前线程执行，不经过 broker worker，主要用于调试场景</li>
</ol>

<h3 id="12-失败重试任务">1.2. 失败重试任务</h3>

<p>普通场景下 Celery 的任务方法，实现与普通函数一样，但是随着深入，比如重试的写法就不同。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">task</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">max_retries</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">default_retry_delay</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">failing_task</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">task_logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">"This task will fail and retry, self.request : </span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">request</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"Intentional error"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">ValueError</span> <span class="k">as</span> <span class="n">exc</span><span class="p">:</span>
        <span class="n">task_logger</span><span class="p">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s">"Task failed. Retrying in </span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">default_retry_delay</span><span class="si">}</span><span class="s"> seconds..."</span><span class="p">)</span>
        <span class="k">raise</span> <span class="bp">self</span><span class="p">.</span><span class="n">retry</span><span class="p">(</span><span class="n">exc</span><span class="o">=</span><span class="n">exc</span><span class="p">)</span>
</code></pre></div></div>

<p>上面是一个重试方法的例子，区别在于指定了<code class="language-plaintext highlighter-rouge">bind=True</code>，以及采用<code class="language-plaintext highlighter-rouge">self.retry</code>重试。</p>

<p>如果是自定义函数，可能会采用<code class="language-plaintext highlighter-rouge">@retry</code>或者 sleep 以达到重试的效果，但是这样<strong>最大的问题是占着 Worker 并发</strong>。</p>

<p><code class="language-plaintext highlighter-rouge">self.retry</code>并不会立即重试，而是增加重试时间后，重新推回了队列。</p>

<p>执行上述代码，观察 Worker 的日志：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[2026-01-11 21:43:01,135: INFO/MainProcess] Task app.tasks.failing_task[4f524ae5-9df0-47bd-b3a7-2ad5e683ab35] received
2026-01-11 21:43:01,135 - celery.tasks - [ThreadPoolExecutor-0_0] - INFO - This task will fail and retry, self.request : ... 'id': '4f524ae5-9df0-47bd-b3a7-2ad5e683ab35', ...
[2026-01-11 21:43:01,135: INFO/MainProcess] This task will fail and retry, self.request : ... 'id': '4f524ae5-9df0-47bd-b3a7-2ad5e683ab35', 'delivery_tag': '31633d28-5ee1-4051-a25c-be4b878059cd' ...
2026-01-11 21:43:01,136 - celery.tasks - [ThreadPoolExecutor-0_0] - WARNING - Task failed. Retrying in 60 seconds...
[2026-01-11 21:43:01,136: WARNING/MainProcess] Task failed. Retrying in 60 seconds...
[2026-01-11 21:43:01,174: INFO/MainProcess] Task app.tasks.failing_task[4f524ae5-9df0-47bd-b3a7-2ad5e683ab35] received
[2026-01-11 21:43:01,182: INFO/MainProcess] Task app.tasks.failing_task[4f524ae5-9df0-47bd-b3a7-2ad5e683ab35] retry: Retry in 60s: ValueError('Intentional error')

2026-01-11 21:44:01,142 - celery.tasks - [ThreadPoolExecutor-0_0] - INFO - This task will fail and retry, self.request : ... 'id': '4f524ae5-9df0-47bd-b3a7-2ad5e683ab35', 'delivery_tag': '31633d28-5ee1-4051-a25c-be4b878059cd' ...
</code></pre></div></div>

<p>可以看到任务 <code class="language-plaintext highlighter-rouge">id=4f524ae5-9df0-47bd-b3a7-2ad5e683ab35</code> 不变，重试是通过 Celery 的机制完成，<strong>因此重试期间不会占用 Worker 并发</strong>。</p>

<p>由于我们当前使用的是 Redis Broker，从 Redis 也可以大概猜测其实现方式：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1:6379&gt; zrange unacked_index 0 <span class="nt">-1</span> withscores
1<span class="o">)</span> <span class="s2">"31633d28-5ee1-4051-a25c-be4b878059cd"</span>
2<span class="o">)</span> <span class="s2">"1.7681390411489222e+9"</span>
</code></pre></div></div>

<p>delivery_tag 的 score 正好对应了任务重试时，下次执行的时间。</p>

<h3 id="13-bind-任务">1.3. bind 任务</h3>

<p>上一节里可以看到<code class="language-plaintext highlighter-rouge">bind=True</code>的参数，该参数的效果使得原本函数本身支持了上下文，因此也就可以调用<code class="language-plaintext highlighter-rouge">self.retry</code>.</p>

<p>bind 之后的函数可以获取到更多上下文参数，可以用于自定义的场景：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">task</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">bind_task</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">i</span><span class="p">):</span>
    <span class="n">task_logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">'i : </span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s"> , self.request : </span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">request</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
</code></pre></div></div>

<p>常用的有：</p>

<table>
  <thead>
    <tr>
      <th>属性</th>
      <th>含义</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">self.request.id</code></td>
      <td>当前 task id</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">self.request.retries</code></td>
      <td>已重试次数</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">self.request.hostname</code></td>
      <td>哪个 worker 在跑</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">self.request.argsrepr</code></td>
      <td>原始参数</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">self.request.kwargsrepr</code></td>
      <td>原始 kwargs</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">self.request.delivery_info</code></td>
      <td>queue / routing</td>
    </tr>
  </tbody>
</table>

<h3 id="14-如何保证任务不丢">1.4. 如何保证任务不丢</h3>

<p>保证任务不丢的实现方式，在于<strong>任务信息应当何时从 Broker 删除？</strong>，是 Worker 取走任务还是 Worker 执行完成任务。</p>

<p>答案显然是后者，我们可以用如下例子来验证：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">task</span>
<span class="k">def</span> <span class="nf">no_ack_task</span><span class="p">():</span>
    <span class="n">task_logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"This task will not be acknowledged."</span><span class="p">)</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span>
    <span class="n">task_logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"No acknowledgment task completed successfully."</span><span class="p">)</span>
    <span class="k">return</span> <span class="s">"No acknowledgment"</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">task</span><span class="p">(</span><span class="n">acks_late</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">ack_task</span><span class="p">():</span>
    <span class="n">task_logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"This task demonstrates the acknowledgment mechanism."</span><span class="p">)</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span>
    <span class="n">task_logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">"Ack task completed successfully."</span><span class="p">)</span>
    <span class="k">return</span> <span class="s">"Acknowledged"</span>
</code></pre></div></div>

<p>启动 Worker(至少两个并发)，提交上述任务，通过日志验证任务已经在 Worker 执行。</p>

<p>此时重启 Worker，只有<code class="language-plaintext highlighter-rouge">ack_task</code>会重试。</p>

<p>当然这里只能确保 AtLeastOnce 的语义，比如 Worker 闪断导致判定失败，就会存在重复执行，此时则依赖<code class="language-plaintext highlighter-rouge">ack_task</code>在实现上是幂等的。</p>

<h3 id="15-周期任务">1.5. 周期任务</h3>

<p>Celery 是通过 Beat 支持定时任务的，启动方式：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># run_beat.sh</span>
<span class="nb">echo</span> <span class="s2">"Starting Celery beat scheduler..."</span>

<span class="nb">export </span><span class="nv">PYTHONPATH</span><span class="o">=</span><span class="s2">"../..:</span><span class="nv">$PYTHONPATH</span><span class="s2">"</span>
../../.venv/bin/celery <span class="nt">-A</span> app.celery_app beat <span class="nt">-l</span> info
</code></pre></div></div>

<p>Beat 的原理，其实就是充当了生产者的角色，按照周期时间将任务提交的队列，Beat 本身不执行任务。</p>

<p>基于此设计，<strong>Beat 只能是单点执行</strong>。</p>

<h2 id="2-可观测">2. 可观测</h2>

<p><strong>一个系统从功能可用，到生产环境可用，中间还要经过 N 多验证，比如性能测试、容错测试、第三方依赖故障测试等、以及可观测性的评估</strong>。</p>

<p>很多时候我们可能因为时间问题直接跳过，但是作业终究还是需要补的。</p>

<p>Celery 提供了一些常用命令，用于了解任务队列状态，例如：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>celery <span class="nt">-A</span> bisheng.worker.main inspect active       <span class="c"># 查看当前正在执行的任务</span>
celery <span class="nt">-A</span> bisheng.worker.main inspect reserved     <span class="c"># 查看已接收但尚未执行（预取）的任务</span>
celery <span class="nt">-A</span> bisheng.worker.main inspect scheduled    <span class="c"># 查看 ETA／countdown 延时任务</span>
celery <span class="nt">-A</span> bisheng.worker.main inspect active_queues <span class="c"># 查看每个 worker 所在队列及消费情况</span>
</code></pre></div></div>

<p>如果未调度（即还在 Broker 里），则需要查看对应的 Broker 存储。</p>

<p>举个例子，比如如果队列积压，任务可能存在于两处：</p>
<ol>
  <li>Celery 已分配给 Worker</li>
  <li>Celery 未分配给 Worker</li>
</ol>

<p>前者查看：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╰─<span class="nv">$ </span>celery <span class="nt">-A</span> app.celery_app inspect reserved
-&gt;  celery@yingzdeMacBook-Pro.local: OK
    <span class="k">*</span> <span class="o">{</span><span class="s1">'id'</span>: <span class="s1">'8a649961-0e21-4278-8788-f8fe369f46e4'</span>, <span class="s1">'name'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'args'</span>: <span class="o">[</span>1, 1], <span class="s1">'kwargs'</span>: <span class="o">{}</span>, <span class="s1">'type'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'hostname'</span>: <span class="s1">'celery@yingzdeMacBook-Pro.local'</span>, <span class="s1">'time_start'</span>: None, <span class="s1">'acknowledged'</span>: False, <span class="s1">'delivery_info'</span>: <span class="o">{</span><span class="s1">'exchange'</span>: <span class="s1">''</span>, <span class="s1">'routing_key'</span>: <span class="s1">'default'</span>, <span class="s1">'priority'</span>: 0, <span class="s1">'redelivered'</span>: False<span class="o">}</span>, <span class="s1">'worker_pid'</span>: None<span class="o">}</span>
    <span class="k">*</span> <span class="o">{</span><span class="s1">'id'</span>: <span class="s1">'49a33ae1-44cd-4d83-a99a-5dbe85f2fd96'</span>, <span class="s1">'name'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'args'</span>: <span class="o">[</span>2, 2], <span class="s1">'kwargs'</span>: <span class="o">{}</span>, <span class="s1">'type'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'hostname'</span>: <span class="s1">'celery@yingzdeMacBook-Pro.local'</span>, <span class="s1">'time_start'</span>: None, <span class="s1">'acknowledged'</span>: False, <span class="s1">'delivery_info'</span>: <span class="o">{</span><span class="s1">'exchange'</span>: <span class="s1">''</span>, <span class="s1">'routing_key'</span>: <span class="s1">'default'</span>, <span class="s1">'priority'</span>: 0, <span class="s1">'redelivered'</span>: False<span class="o">}</span>, <span class="s1">'worker_pid'</span>: None<span class="o">}</span>
    <span class="k">*</span> <span class="o">{</span><span class="s1">'id'</span>: <span class="s1">'ba4cfb3c-c46d-475d-9700-b9ee00d022ff'</span>, <span class="s1">'name'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'args'</span>: <span class="o">[</span>4, 4], <span class="s1">'kwargs'</span>: <span class="o">{}</span>, <span class="s1">'type'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'hostname'</span>: <span class="s1">'celery@yingzdeMacBook-Pro.local'</span>, <span class="s1">'time_start'</span>: None, <span class="s1">'acknowledged'</span>: False, <span class="s1">'delivery_info'</span>: <span class="o">{</span><span class="s1">'exchange'</span>: <span class="s1">''</span>, <span class="s1">'routing_key'</span>: <span class="s1">'default'</span>, <span class="s1">'priority'</span>: 0, <span class="s1">'redelivered'</span>: False<span class="o">}</span>, <span class="s1">'worker_pid'</span>: None<span class="o">}</span>
    <span class="k">*</span> <span class="o">{</span><span class="s1">'id'</span>: <span class="s1">'5a43dfee-db9f-4144-9ede-061e11321620'</span>, <span class="s1">'name'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'args'</span>: <span class="o">[</span>3, 3], <span class="s1">'kwargs'</span>: <span class="o">{}</span>, <span class="s1">'type'</span>: <span class="s1">'app.tasks.add'</span>, <span class="s1">'hostname'</span>: <span class="s1">'celery@yingzdeMacBook-Pro.local'</span>, <span class="s1">'time_start'</span>: None, <span class="s1">'acknowledged'</span>: False, <span class="s1">'delivery_info'</span>: <span class="o">{</span><span class="s1">'exchange'</span>: <span class="s1">''</span>, <span class="s1">'routing_key'</span>: <span class="s1">'default'</span>, <span class="s1">'priority'</span>: 0, <span class="s1">'redelivered'</span>: False<span class="o">}</span>, <span class="s1">'worker_pid'</span>: None<span class="o">}</span>
</code></pre></div></div>

<p>后者则需要直接查看 Redis：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1:6379&gt; lrange default 0 <span class="nt">-1</span>
1<span class="o">)</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">body</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">W1s5LCA5XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">utf-8</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-type</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">application/json</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">headers</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">lang</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">py</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">task</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">app.tasks.add</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">919ecb65-33e3-4f91-a6ea-2f060f79135a</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">shadow</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">eta</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">expires</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group_index</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">retries</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">timelimit</span><span class="se">\"</span><span class="s2">: [null, null], </span><span class="se">\"</span><span class="s2">root_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">919ecb65-33e3-4f91-a6ea-2f060f79135a</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">parent_id</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">argsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">(9, 9)</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">kwargsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">{}</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">origin</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">gen13764@yingzdeMacBook-Pro.local</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">ignore_result</span><span class="se">\"</span><span class="s2">: false, </span><span class="se">\"</span><span class="s2">replaced_task_nesting</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">stamped_headers</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">stamps</span><span class="se">\"</span><span class="s2">: {}}, </span><span class="se">\"</span><span class="s2">properties</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">correlation_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">919ecb65-33e3-4f91-a6ea-2f060f79135a</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">reply_to</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">d1f96e44-82f3-3ea5-b912-5909a6f90f42</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_mode</span><span class="se">\"</span><span class="s2">: 2, </span><span class="se">\"</span><span class="s2">delivery_info</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">exchange</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">routing_key</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">default</span><span class="se">\"</span><span class="s2">}, </span><span class="se">\"</span><span class="s2">priority</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">body_encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">base64</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_tag</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">b0b55f87-981f-443b-bcf8-679ca2e8b159</span><span class="se">\"</span><span class="s2">}}"</span>
2<span class="o">)</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">body</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">W1s4LCA4XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">utf-8</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-type</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">application/json</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">headers</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">lang</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">py</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">task</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">app.tasks.add</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">643637da-27a5-44d5-9930-8bd8bed72cee</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">shadow</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">eta</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">expires</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group_index</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">retries</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">timelimit</span><span class="se">\"</span><span class="s2">: [null, null], </span><span class="se">\"</span><span class="s2">root_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">643637da-27a5-44d5-9930-8bd8bed72cee</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">parent_id</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">argsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">(8, 8)</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">kwargsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">{}</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">origin</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">gen13764@yingzdeMacBook-Pro.local</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">ignore_result</span><span class="se">\"</span><span class="s2">: false, </span><span class="se">\"</span><span class="s2">replaced_task_nesting</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">stamped_headers</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">stamps</span><span class="se">\"</span><span class="s2">: {}}, </span><span class="se">\"</span><span class="s2">properties</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">correlation_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">643637da-27a5-44d5-9930-8bd8bed72cee</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">reply_to</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">d1f96e44-82f3-3ea5-b912-5909a6f90f42</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_mode</span><span class="se">\"</span><span class="s2">: 2, </span><span class="se">\"</span><span class="s2">delivery_info</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">exchange</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">routing_key</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">default</span><span class="se">\"</span><span class="s2">}, </span><span class="se">\"</span><span class="s2">priority</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">body_encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">base64</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_tag</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">ccbac1d6-a32b-41b4-997c-8603616d6e3c</span><span class="se">\"</span><span class="s2">}}"</span>
3<span class="o">)</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">body</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">W1s3LCA3XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">utf-8</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-type</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">application/json</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">headers</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">lang</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">py</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">task</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">app.tasks.add</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">756de473-96bf-4643-95a0-702b96b6afae</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">shadow</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">eta</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">expires</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group_index</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">retries</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">timelimit</span><span class="se">\"</span><span class="s2">: [null, null], </span><span class="se">\"</span><span class="s2">root_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">756de473-96bf-4643-95a0-702b96b6afae</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">parent_id</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">argsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">(7, 7)</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">kwargsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">{}</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">origin</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">gen13764@yingzdeMacBook-Pro.local</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">ignore_result</span><span class="se">\"</span><span class="s2">: false, </span><span class="se">\"</span><span class="s2">replaced_task_nesting</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">stamped_headers</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">stamps</span><span class="se">\"</span><span class="s2">: {}}, </span><span class="se">\"</span><span class="s2">properties</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">correlation_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">756de473-96bf-4643-95a0-702b96b6afae</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">reply_to</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">d1f96e44-82f3-3ea5-b912-5909a6f90f42</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_mode</span><span class="se">\"</span><span class="s2">: 2, </span><span class="se">\"</span><span class="s2">delivery_info</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">exchange</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">routing_key</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">default</span><span class="se">\"</span><span class="s2">}, </span><span class="se">\"</span><span class="s2">priority</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">body_encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">base64</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_tag</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">dea6405f-3a88-4f5c-9e55-102a319f29aa</span><span class="se">\"</span><span class="s2">}}"</span>
4<span class="o">)</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">body</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">W1s2LCA2XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">utf-8</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-type</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">application/json</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">headers</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">lang</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">py</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">task</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">app.tasks.add</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">e3f3cc57-62da-4a97-8a05-a099a0a0e49b</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">shadow</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">eta</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">expires</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group_index</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">retries</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">timelimit</span><span class="se">\"</span><span class="s2">: [null, null], </span><span class="se">\"</span><span class="s2">root_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">e3f3cc57-62da-4a97-8a05-a099a0a0e49b</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">parent_id</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">argsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">(6, 6)</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">kwargsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">{}</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">origin</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">gen13764@yingzdeMacBook-Pro.local</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">ignore_result</span><span class="se">\"</span><span class="s2">: false, </span><span class="se">\"</span><span class="s2">replaced_task_nesting</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">stamped_headers</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">stamps</span><span class="se">\"</span><span class="s2">: {}}, </span><span class="se">\"</span><span class="s2">properties</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">correlation_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">e3f3cc57-62da-4a97-8a05-a099a0a0e49b</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">reply_to</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">d1f96e44-82f3-3ea5-b912-5909a6f90f42</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_mode</span><span class="se">\"</span><span class="s2">: 2, </span><span class="se">\"</span><span class="s2">delivery_info</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">exchange</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">routing_key</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">default</span><span class="se">\"</span><span class="s2">}, </span><span class="se">\"</span><span class="s2">priority</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">body_encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">base64</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_tag</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">2decdf7b-153b-409e-beb3-b4a3bc396472</span><span class="se">\"</span><span class="s2">}}"</span>
5<span class="o">)</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">body</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">W1s1LCA1XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">utf-8</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">content-type</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">application/json</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">headers</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">lang</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">py</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">task</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">app.tasks.add</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">7e96b5a6-2972-4931-829c-4a5d29be5ec9</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">shadow</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">eta</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">expires</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">group_index</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">retries</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">timelimit</span><span class="se">\"</span><span class="s2">: [null, null], </span><span class="se">\"</span><span class="s2">root_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">7e96b5a6-2972-4931-829c-4a5d29be5ec9</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">parent_id</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">argsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">(5, 5)</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">kwargsrepr</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">{}</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">origin</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">gen13764@yingzdeMacBook-Pro.local</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">ignore_result</span><span class="se">\"</span><span class="s2">: false, </span><span class="se">\"</span><span class="s2">replaced_task_nesting</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">stamped_headers</span><span class="se">\"</span><span class="s2">: null, </span><span class="se">\"</span><span class="s2">stamps</span><span class="se">\"</span><span class="s2">: {}}, </span><span class="se">\"</span><span class="s2">properties</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">correlation_id</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">7e96b5a6-2972-4931-829c-4a5d29be5ec9</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">reply_to</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">d1f96e44-82f3-3ea5-b912-5909a6f90f42</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_mode</span><span class="se">\"</span><span class="s2">: 2, </span><span class="se">\"</span><span class="s2">delivery_info</span><span class="se">\"</span><span class="s2">: {</span><span class="se">\"</span><span class="s2">exchange</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">routing_key</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">default</span><span class="se">\"</span><span class="s2">}, </span><span class="se">\"</span><span class="s2">priority</span><span class="se">\"</span><span class="s2">: 0, </span><span class="se">\"</span><span class="s2">body_encoding</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">base64</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">delivery_tag</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">dad2f2be-4f8a-4c71-a647-5c650a1d6b05</span><span class="se">\"</span><span class="s2">}}"</span>
</code></pre></div></div>

<p>这种方式操作不便，同时可以看到跟 Broker 类型强绑定。</p>

<p>Celery 通过提供 <strong>flower 解决了状态可视化的问题</strong>。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╰─$ celery -A app.celery_app flower

[I 260111 22:54:13 command:168] Visit me at http://0.0.0.0:5555
[I 260111 22:54:13 command:176] Broker: redis://localhost:6379/0
[I 260111 22:54:13 command:177] Registered tasks:
    ['app.tasks.ack_task',
     'app.tasks.add',
     'app.tasks.bind_task',
     'app.tasks.failing_task',
     'app.tasks.log_message',
     'app.tasks.no_ack_task',
     'app.tasks.periodic_task',
     'app.tasks.sqrt_task',
     'celery.accumulate',
     'celery.backend_cleanup',
     'celery.chain',
     'celery.chord',
     'celery.chord_unlock',
     'celery.chunks',
     'celery.group',
     'celery.map',
     'celery.starmap']
[I 260111 22:54:13 mixins:228] Connected to redis://localhost:6379/0
</code></pre></div></div>

<p>启动后，通过 flower 页面，可以方便的看到 Worker Tasks Broker 的情况。</p>

<p><img src="/assets/images/celery/flower_web.png" alt="" /></p>

<p>比如当我们一次性推送较多任务到 Broker，可以看到任务的情况：</p>

<p><img src="/assets/images/celery/flower_web_2.png" alt="" /></p>

<p>对于尚未分发出来的任务，则可以通过 Broker 页面看到积压的任务数：</p>

<p><img src="/assets/images/celery/flower_web_broker.png" alt="" /></p>

<p>状态可视化解决了，那异常时如何报警？比如当队列数超过 1000 则认为系统出现问题。</p>

<p>这块不得不说 Prometheus 协议的强大之处，flower 同时启动了 <a href="http://0.0.0.0:5555/metrics">http://0.0.0.0:5555/metrics</a> 地址提供了符合 Prometheus 抓取格式的数据，因此就可以天然对接 Prometheus 了:</p>

<p><img src="/assets/images/celery/celery_flower_metrics.png" alt="" /></p>

<p>当然这里如果想要放到生产环境，需要确保只采集单个 flower 实例的指标数据。</p>

<h2 id="3-其他">3. 其他</h2>

<p>我们使用 Celery 最直接的是 queue，但是实际上观察前面打印的 delivery_info ，可以看到包含了 routing_key exchange 等信息，</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>task
  └─ routing_key
        ↓
     exchange
        ↓（匹配规则）
      queue
        ↓
      worker
</code></pre></div></div>

<p>routing_key 的设计好处，在于提供了一个配置 routing_key -&gt; queue 的映射关系。</p>

<p>比如我们希望处理不同 level 的日志，写入不同的 queue</p>

<p>如果是自己维护的话，可能是这么写：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>process_log.apply_async(queue=f'{level}_logs')
</code></pre></div></div>

<p>但是采用<code class="language-plaintext highlighter-rouge">routing_key</code>的这种配置化写法，则灵活性更高，能应对更复杂的情况。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   process_log.apply_async(exchange='logs', routing_key=level, ...)

   task_queues=(
       Queue('error_logs',   logs_exchange, routing_key='error'),
       Queue('warning_logs', logs_exchange, routing_key='warning'),
  
       # 将 'info' 和 'debug' 这两个不同的路由键，都指向同一个队列
       Queue('transient_logs', logs_exchange, routing_key='info'),
       Queue('transient_logs', logs_exchange, routing_key='debug'),
   )
</code></pre></div></div>

<p>此外我在研究 Celery 时，发现不同的 backend，由于特性不同，支持的能力也有差别，例如 reject_on_worker_lost 等参数；以及如何指定任务的优先级、如何结合其他系统实现动态队列、动态 Worker 等，这些是否还是 Celery 系统所擅长解决的？就需要进一步研究了。</p>]]></content><author><name>ying</name></author><category term="microservice" /><summary type="html"><![CDATA[上一篇介绍了 Celery 架构，这篇我们实战看看。 1. 例子 文章从基础、重试、ack、周期任务等几个方面说明，完整的运行例子放在了 github 上 1.1. Hello World 入门例子使用非常简洁，分为两步： 启动Worker: 从 Redis 读取任务，执行add方法，将结果写回 Redis run_simple_task -&gt; add.delay: 写入任务，通过app分发到 default 队列(存储到 Redis)，并等待读取结果 1.1.1. 消费者-Worker 先从启动 Worker 开始，定义app/celery_app.py： #!/usr/bin/env python # coding=utf-8 from celery import Celery from kombu import Queue, Exchange app = Celery('demo_celery', broker='redis://localhost/0', backend='redis://localhost/0', include=['app.tasks']) 指定app.celery_app，启动 Worker ： ╰─$ export PYTHONPATH="../..:$PYTHONPATH" ../../.venv/bin/celery -A app.celery_app worker -l info -P threads -Q default,periodic_tasks -c 1 celery@yingzdeMacBook-Pro.local v5.5.3 (immunity) macOS-10.16-x86_64-i386-64bit 2026-01-11 19:08:19 [config] .&gt; app: demo_celery:0x10741d9a0 .&gt; transport: redis://localhost:6379/0 .&gt; results: redis://localhost/0 .&gt; concurrency: 1 (thread) .&gt; task events: OFF (enable -E to monitor tasks in this worker) [queues] .&gt; default exchange=default(direct) key=default .&gt; periodic_tasks exchange=periodic_tasks(direct) key=periodic_tasks [tasks] . app.tasks.ack_task . app.tasks.add . app.tasks.bind_task . app.tasks.failing_task . app.tasks.log_message . app.tasks.no_ack_task . app.tasks.periodic_task . app.tasks.sqrt_task [2026-01-11 19:08:19,574: INFO/MainProcess] Connected to redis://localhost:6379/0 [2026-01-11 19:08:19,577: INFO/MainProcess] mingle: searching for neighbors [2026-01-11 19:08:20,582: INFO/MainProcess] mingle: all alone [2026-01-11 19:08:20,597: INFO/MainProcess] celery@yingzdeMacBook-Pro.local ready. 1.1.2. 生产者 首先需要定义任务本身，写法跟普通函数一样，唯一的区别在于装饰器里指定了 Celery 对象名，即上一节定义的app = Celery(...)： from py3_journey.demo_celery.app.celery_app import app @app.task def add(x, y): time.sleep(50) task_logger.info(f"Adding {x} and {y}") return x + y 然后调用add.delay方法 def run_simple_task(): logger.info("Dispatching simple 'add' task...") # using delay to trigger a task result = add.delay(4, 4) logger.info(f"Task dispatched. Task ID: {result.id}. Waiting for result...") logger.info(f"Result: {result.get(timeout=10)}") 执行run_simple_task就可以看到 Worker 的输出了： [2026-01-11 21:17:55,407: INFO/MainProcess] Task app.tasks.add[4ae42872-1285-4599-b23a-f4f29f6dd7f9] received 2026-01-11 21:18:45,411 - celery.tasks - [ThreadPoolExecutor-0_0] - INFO - Adding 4 and 4 [2026-01-11 21:18:45,411: INFO/MainProcess] Adding 4 and 4 [2026-01-11 21:18:45,474: INFO/MainProcess] Task app.tasks.add[4ae42872-1285-4599-b23a-f4f29f6dd7f9] succeeded in 50.06529796200084s: 8 Worker 执行完成后，也就看到了 Result 的日志： 2026-01-11 21:17:55,275 - Dispatching simple 'add' task... 2026-01-11 21:17:55,407 - Task dispatched. Task ID: 4ae42872-1285-4599-b23a-f4f29f6dd7f9. Waiting for result... 2026-01-11 21:18:45,474 - Result: 8 从之前的分析知道add是在 Worker 执行的，所以如果修改了add这类执行代码，需要重启/热加载 Worker。 提交任务的方式有多种： app.send_task: 任务名作为字符串形式，好处是解耦，不需要在一个代码库 ，缺点则是 task 改名、找到谁在调用等运维问题 task.apply_async(): 标准方式，有 task 对象； task.delay(): apply_async 的简化版，简洁、不能指定复杂参数 task.apply: 在当前线程执行，不经过 broker worker，主要用于调试场景 1.2. 失败重试任务 普通场景下 Celery 的任务方法，实现与普通函数一样，但是随着深入，比如重试的写法就不同。 @app.task(bind=True, max_retries=3, default_retry_delay=60) def failing_task(self): try: task_logger.info(f"This task will fail and retry, self.request : {self.request}") raise ValueError("Intentional error") except ValueError as exc: task_logger.warning(f"Task failed. Retrying in {self.default_retry_delay} seconds...") raise self.retry(exc=exc) 上面是一个重试方法的例子，区别在于指定了bind=True，以及采用self.retry重试。 如果是自定义函数，可能会采用@retry或者 sleep 以达到重试的效果，但是这样最大的问题是占着 Worker 并发。 self.retry并不会立即重试，而是增加重试时间后，重新推回了队列。 执行上述代码，观察 Worker 的日志： [2026-01-11 21:43:01,135: INFO/MainProcess] Task app.tasks.failing_task[4f524ae5-9df0-47bd-b3a7-2ad5e683ab35] received 2026-01-11 21:43:01,135 - celery.tasks - [ThreadPoolExecutor-0_0] - INFO - This task will fail and retry, self.request : ... 'id': '4f524ae5-9df0-47bd-b3a7-2ad5e683ab35', ... [2026-01-11 21:43:01,135: INFO/MainProcess] This task will fail and retry, self.request : ... 'id': '4f524ae5-9df0-47bd-b3a7-2ad5e683ab35', 'delivery_tag': '31633d28-5ee1-4051-a25c-be4b878059cd' ... 2026-01-11 21:43:01,136 - celery.tasks - [ThreadPoolExecutor-0_0] - WARNING - Task failed. Retrying in 60 seconds... [2026-01-11 21:43:01,136: WARNING/MainProcess] Task failed. Retrying in 60 seconds... [2026-01-11 21:43:01,174: INFO/MainProcess] Task app.tasks.failing_task[4f524ae5-9df0-47bd-b3a7-2ad5e683ab35] received [2026-01-11 21:43:01,182: INFO/MainProcess] Task app.tasks.failing_task[4f524ae5-9df0-47bd-b3a7-2ad5e683ab35] retry: Retry in 60s: ValueError('Intentional error') 2026-01-11 21:44:01,142 - celery.tasks - [ThreadPoolExecutor-0_0] - INFO - This task will fail and retry, self.request : ... 'id': '4f524ae5-9df0-47bd-b3a7-2ad5e683ab35', 'delivery_tag': '31633d28-5ee1-4051-a25c-be4b878059cd' ... 可以看到任务 id=4f524ae5-9df0-47bd-b3a7-2ad5e683ab35 不变，重试是通过 Celery 的机制完成，因此重试期间不会占用 Worker 并发。 由于我们当前使用的是 Redis Broker，从 Redis 也可以大概猜测其实现方式： 127.0.0.1:6379&gt; zrange unacked_index 0 -1 withscores 1) "31633d28-5ee1-4051-a25c-be4b878059cd" 2) "1.7681390411489222e+9" delivery_tag 的 score 正好对应了任务重试时，下次执行的时间。 1.3. bind 任务 上一节里可以看到bind=True的参数，该参数的效果使得原本函数本身支持了上下文，因此也就可以调用self.retry. bind 之后的函数可以获取到更多上下文参数，可以用于自定义的场景： @app.task(bind=True) def bind_task(self, i): task_logger.info(f'i : {i} , self.request : {self.request}') 常用的有： 属性 含义 self.request.id 当前 task id self.request.retries 已重试次数 self.request.hostname 哪个 worker 在跑 self.request.argsrepr 原始参数 self.request.kwargsrepr 原始 kwargs self.request.delivery_info queue / routing 1.4. 如何保证任务不丢 保证任务不丢的实现方式，在于任务信息应当何时从 Broker 删除？，是 Worker 取走任务还是 Worker 执行完成任务。 答案显然是后者，我们可以用如下例子来验证： @app.task def no_ack_task(): task_logger.info("This task will not be acknowledged.") time.sleep(30) task_logger.info("No acknowledgment task completed successfully.") return "No acknowledgment" @app.task(acks_late=True) def ack_task(): task_logger.info("This task demonstrates the acknowledgment mechanism.") time.sleep(30) task_logger.info("Ack task completed successfully.") return "Acknowledged" 启动 Worker(至少两个并发)，提交上述任务，通过日志验证任务已经在 Worker 执行。 此时重启 Worker，只有ack_task会重试。 当然这里只能确保 AtLeastOnce 的语义，比如 Worker 闪断导致判定失败，就会存在重复执行，此时则依赖ack_task在实现上是幂等的。 1.5. 周期任务 Celery 是通过 Beat 支持定时任务的，启动方式： #!/bin/bash # run_beat.sh echo "Starting Celery beat scheduler..." export PYTHONPATH="../..:$PYTHONPATH" ../../.venv/bin/celery -A app.celery_app beat -l info Beat 的原理，其实就是充当了生产者的角色，按照周期时间将任务提交的队列，Beat 本身不执行任务。 基于此设计，Beat 只能是单点执行。 2. 可观测 一个系统从功能可用，到生产环境可用，中间还要经过 N 多验证，比如性能测试、容错测试、第三方依赖故障测试等、以及可观测性的评估。 很多时候我们可能因为时间问题直接跳过，但是作业终究还是需要补的。 Celery 提供了一些常用命令，用于了解任务队列状态，例如： celery -A bisheng.worker.main inspect active # 查看当前正在执行的任务 celery -A bisheng.worker.main inspect reserved # 查看已接收但尚未执行（预取）的任务 celery -A bisheng.worker.main inspect scheduled # 查看 ETA／countdown 延时任务 celery -A bisheng.worker.main inspect active_queues # 查看每个 worker 所在队列及消费情况 如果未调度（即还在 Broker 里），则需要查看对应的 Broker 存储。 举个例子，比如如果队列积压，任务可能存在于两处： Celery 已分配给 Worker Celery 未分配给 Worker 前者查看： ╰─$ celery -A app.celery_app inspect reserved -&gt; celery@yingzdeMacBook-Pro.local: OK * {'id': '8a649961-0e21-4278-8788-f8fe369f46e4', 'name': 'app.tasks.add', 'args': [1, 1], 'kwargs': {}, 'type': 'app.tasks.add', 'hostname': 'celery@yingzdeMacBook-Pro.local', 'time_start': None, 'acknowledged': False, 'delivery_info': {'exchange': '', 'routing_key': 'default', 'priority': 0, 'redelivered': False}, 'worker_pid': None} * {'id': '49a33ae1-44cd-4d83-a99a-5dbe85f2fd96', 'name': 'app.tasks.add', 'args': [2, 2], 'kwargs': {}, 'type': 'app.tasks.add', 'hostname': 'celery@yingzdeMacBook-Pro.local', 'time_start': None, 'acknowledged': False, 'delivery_info': {'exchange': '', 'routing_key': 'default', 'priority': 0, 'redelivered': False}, 'worker_pid': None} * {'id': 'ba4cfb3c-c46d-475d-9700-b9ee00d022ff', 'name': 'app.tasks.add', 'args': [4, 4], 'kwargs': {}, 'type': 'app.tasks.add', 'hostname': 'celery@yingzdeMacBook-Pro.local', 'time_start': None, 'acknowledged': False, 'delivery_info': {'exchange': '', 'routing_key': 'default', 'priority': 0, 'redelivered': False}, 'worker_pid': None} * {'id': '5a43dfee-db9f-4144-9ede-061e11321620', 'name': 'app.tasks.add', 'args': [3, 3], 'kwargs': {}, 'type': 'app.tasks.add', 'hostname': 'celery@yingzdeMacBook-Pro.local', 'time_start': None, 'acknowledged': False, 'delivery_info': {'exchange': '', 'routing_key': 'default', 'priority': 0, 'redelivered': False}, 'worker_pid': None} 后者则需要直接查看 Redis： 127.0.0.1:6379&gt; lrange default 0 -1 1) "{\"body\": \"W1s5LCA5XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"app.tasks.add\", \"id\": \"919ecb65-33e3-4f91-a6ea-2f060f79135a\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"group_index\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"919ecb65-33e3-4f91-a6ea-2f060f79135a\", \"parent_id\": null, \"argsrepr\": \"(9, 9)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen13764@yingzdeMacBook-Pro.local\", \"ignore_result\": false, \"replaced_task_nesting\": 0, \"stamped_headers\": null, \"stamps\": {}}, \"properties\": {\"correlation_id\": \"919ecb65-33e3-4f91-a6ea-2f060f79135a\", \"reply_to\": \"d1f96e44-82f3-3ea5-b912-5909a6f90f42\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"default\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"b0b55f87-981f-443b-bcf8-679ca2e8b159\"}}" 2) "{\"body\": \"W1s4LCA4XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"app.tasks.add\", \"id\": \"643637da-27a5-44d5-9930-8bd8bed72cee\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"group_index\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"643637da-27a5-44d5-9930-8bd8bed72cee\", \"parent_id\": null, \"argsrepr\": \"(8, 8)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen13764@yingzdeMacBook-Pro.local\", \"ignore_result\": false, \"replaced_task_nesting\": 0, \"stamped_headers\": null, \"stamps\": {}}, \"properties\": {\"correlation_id\": \"643637da-27a5-44d5-9930-8bd8bed72cee\", \"reply_to\": \"d1f96e44-82f3-3ea5-b912-5909a6f90f42\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"default\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"ccbac1d6-a32b-41b4-997c-8603616d6e3c\"}}" 3) "{\"body\": \"W1s3LCA3XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"app.tasks.add\", \"id\": \"756de473-96bf-4643-95a0-702b96b6afae\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"group_index\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"756de473-96bf-4643-95a0-702b96b6afae\", \"parent_id\": null, \"argsrepr\": \"(7, 7)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen13764@yingzdeMacBook-Pro.local\", \"ignore_result\": false, \"replaced_task_nesting\": 0, \"stamped_headers\": null, \"stamps\": {}}, \"properties\": {\"correlation_id\": \"756de473-96bf-4643-95a0-702b96b6afae\", \"reply_to\": \"d1f96e44-82f3-3ea5-b912-5909a6f90f42\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"default\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"dea6405f-3a88-4f5c-9e55-102a319f29aa\"}}" 4) "{\"body\": \"W1s2LCA2XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"app.tasks.add\", \"id\": \"e3f3cc57-62da-4a97-8a05-a099a0a0e49b\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"group_index\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"e3f3cc57-62da-4a97-8a05-a099a0a0e49b\", \"parent_id\": null, \"argsrepr\": \"(6, 6)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen13764@yingzdeMacBook-Pro.local\", \"ignore_result\": false, \"replaced_task_nesting\": 0, \"stamped_headers\": null, \"stamps\": {}}, \"properties\": {\"correlation_id\": \"e3f3cc57-62da-4a97-8a05-a099a0a0e49b\", \"reply_to\": \"d1f96e44-82f3-3ea5-b912-5909a6f90f42\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"default\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"2decdf7b-153b-409e-beb3-b4a3bc396472\"}}" 5) "{\"body\": \"W1s1LCA1XSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"app.tasks.add\", \"id\": \"7e96b5a6-2972-4931-829c-4a5d29be5ec9\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"group_index\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"7e96b5a6-2972-4931-829c-4a5d29be5ec9\", \"parent_id\": null, \"argsrepr\": \"(5, 5)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen13764@yingzdeMacBook-Pro.local\", \"ignore_result\": false, \"replaced_task_nesting\": 0, \"stamped_headers\": null, \"stamps\": {}}, \"properties\": {\"correlation_id\": \"7e96b5a6-2972-4931-829c-4a5d29be5ec9\", \"reply_to\": \"d1f96e44-82f3-3ea5-b912-5909a6f90f42\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"default\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"dad2f2be-4f8a-4c71-a647-5c650a1d6b05\"}}" 这种方式操作不便，同时可以看到跟 Broker 类型强绑定。 Celery 通过提供 flower 解决了状态可视化的问题。 ╰─$ celery -A app.celery_app flower [I 260111 22:54:13 command:168] Visit me at http://0.0.0.0:5555 [I 260111 22:54:13 command:176] Broker: redis://localhost:6379/0 [I 260111 22:54:13 command:177] Registered tasks: ['app.tasks.ack_task', 'app.tasks.add', 'app.tasks.bind_task', 'app.tasks.failing_task', 'app.tasks.log_message', 'app.tasks.no_ack_task', 'app.tasks.periodic_task', 'app.tasks.sqrt_task', 'celery.accumulate', 'celery.backend_cleanup', 'celery.chain', 'celery.chord', 'celery.chord_unlock', 'celery.chunks', 'celery.group', 'celery.map', 'celery.starmap'] [I 260111 22:54:13 mixins:228] Connected to redis://localhost:6379/0 启动后，通过 flower 页面，可以方便的看到 Worker Tasks Broker 的情况。 比如当我们一次性推送较多任务到 Broker，可以看到任务的情况： 对于尚未分发出来的任务，则可以通过 Broker 页面看到积压的任务数： 状态可视化解决了，那异常时如何报警？比如当队列数超过 1000 则认为系统出现问题。 这块不得不说 Prometheus 协议的强大之处，flower 同时启动了 http://0.0.0.0:5555/metrics 地址提供了符合 Prometheus 抓取格式的数据，因此就可以天然对接 Prometheus 了: 当然这里如果想要放到生产环境，需要确保只采集单个 flower 实例的指标数据。 3. 其他 我们使用 Celery 最直接的是 queue，但是实际上观察前面打印的 delivery_info ，可以看到包含了 routing_key exchange 等信息， task └─ routing_key ↓ exchange ↓（匹配规则） queue ↓ worker routing_key 的设计好处，在于提供了一个配置 routing_key -&gt; queue 的映射关系。 比如我们希望处理不同 level 的日志，写入不同的 queue 如果是自己维护的话，可能是这么写： process_log.apply_async(queue=f'{level}_logs') 但是采用routing_key的这种配置化写法，则灵活性更高，能应对更复杂的情况。 process_log.apply_async(exchange='logs', routing_key=level, ...) task_queues=( Queue('error_logs', logs_exchange, routing_key='error'), Queue('warning_logs', logs_exchange, routing_key='warning'), # 将 'info' 和 'debug' 这两个不同的路由键，都指向同一个队列 Queue('transient_logs', logs_exchange, routing_key='info'), Queue('transient_logs', logs_exchange, routing_key='debug'), ) 此外我在研究 Celery 时，发现不同的 backend，由于特性不同，支持的能力也有差别，例如 reject_on_worker_lost 等参数；以及如何指定任务的优先级、如何结合其他系统实现动态队列、动态 Worker 等，这些是否还是 Celery 系统所擅长解决的？就需要进一步研究了。]]></summary></entry><entry><title type="html">任务队列 Celery 架构</title><link href="https://izualzhy.cn/celery-arch" rel="alternate" type="text/html" title="任务队列 Celery 架构" /><published>2026-01-10T16:56:34+00:00</published><updated>2026-01-10T16:56:34+00:00</updated><id>https://izualzhy.cn/celery-arch</id><content type="html" xml:base="https://izualzhy.cn/celery-arch"><![CDATA[<h2 id="1-定位celery-是什么">1. 定位：Celery 是什么？</h2>

<p>Celery 是一个分布式任务队列系统，用于在多个工作进程和机器之间<strong>异步执行任务</strong>。</p>

<p>异步任务的需求很多，特别是耗时且需要平滑处理的场景。比如文件格式转换、数据统计、调用第三方耗时的 api 等，就需要任务入队，然后逐批出队处理。</p>

<p>有些情况下，异步任务还往往伴随着延迟或者周期处理的需求，例如统计网站使用量、文件个数大小等，Celery 也还支持了<strong>周期及延迟任务</strong>。</p>

<h2 id="2-思考如果自己实现">2. 思考：如果自己实现</h2>

<p>实现任务队列，基础是链条上参与的三个角色：<strong>生产者、队列、消费者</strong></p>

<p>首先是生产者：需要支持用户自定义任务，能够方便的提交到任务队列，队列里存储的应当是任务的元信息。这块需要提供代码框架或者接口实现。</p>

<p>其次是队列：任务不丢的基础是持久化。我的第一个想法是采用消息队列存储，Celery 则支持了多种存储格式。</p>

<p>最后是消费者：从队列中取出任务，实际执行用户任务代码。这里简单易用的实现方式是线程池，如果用户代码不可控、或者环境冲突，任务就需要放到容器执行。</p>

<p>基于上述基础功能，我们可能还要考虑很多<strong>实际生产问题</strong>：</p>
<ol>
  <li>是否需要背压：类似流式管道，队列是否是有界的，当生产速度已经远远大于消费速度，是否应当让生产者感知？</li>
  <li>不丢：比如消费者取走任务后，队列服务、消费者服务重启，此时是否会导致丢任务？</li>
  <li>不重：任务不能重复分发，如果是分布式系统下的典型 Unknown 场景，那此时是保证了 Exactly-once 还是 At-least-once ？大部分场景下我们追求后者，转而要求用户任务应当是幂等的。</li>
  <li>失败重试：任务队列是否支持重试任务，还是让任务自身重试？如果任务自身，在任务队列系统看来跟普通执行并没有区别，可能会一直占着并发</li>
  <li>延迟处理：任务队列能否指定该异步任务半小时后执行？</li>
  <li>运行结果：如果是 fire and forget 的任务，不需要结果，可是如果是需要任务运行后的结果呢？类似于 akka 的 ? !</li>
  <li>监控：当前有多少任务执行？多少排队？</li>
</ol>

<h2 id="3-架构celery-怎么实现的">3. 架构：Celery 怎么实现的</h2>

<pre><code class="language-mermaid">graph TB
    subgraph "客户端层"
        ClientApp["客户端应用"]
        TaskDef["任务定义&lt;br/&gt;@app.task装饰器"]
    end
    
    subgraph "Celery应用"
        CeleryApp["Celery&lt;br/&gt;celery/app/base.py"]
        TaskRegistry["任务注册表&lt;br/&gt;celery/app/registry.py"]
        ConfigSystem["配置系统&lt;br/&gt;celery/app/defaults.py"]
        AMQP["AMQP层&lt;br/&gt;celery/app/amqp.py"]
        
        CeleryApp --&gt; TaskRegistry
        CeleryApp --&gt; ConfigSystem
        CeleryApp --&gt; AMQP
    end
    
    subgraph "消息基础设施"
        Broker["消息代理&lt;br/&gt;RabbitMQ/Redis/SQS"]
        Queues["任务队列"]
        Router["消息路由器&lt;br/&gt;celery.app.routes"]
        
        Broker --&gt; Queues
        AMQP --&gt; Router
        Router --&gt; Broker
    end
    
    subgraph "Worker系统"
        WorkerApp["Worker&lt;br/&gt;celery/apps/worker.py"]
        Consumer["消费者&lt;br/&gt;celery/worker/consumer.py"]
        Pool["执行池&lt;br/&gt;prefork/eventlet/gevent"]
        
        WorkerApp --&gt; Consumer
        Consumer --&gt; Pool
    end
    
    subgraph "结果存储"
        Backend["结果后端&lt;br/&gt;celery/backends/base.py"]
        RedisBackend["Redis后端"]
        RPCBackend["RPC后端"]
        DatabaseBackend["数据库后端"]
        
        Backend --&gt; RedisBackend
        Backend --&gt; RPCBackend
        Backend --&gt; DatabaseBackend
    end
    
    subgraph "调度器系统"
        BeatApp["Beat&lt;br/&gt;celery/apps/beat.py"]
        Scheduler["调度器&lt;br/&gt;celery/beat.py"]
        
        BeatApp --&gt; Scheduler
    end
    
    ClientApp --&gt; TaskDef
    TaskDef --&gt; CeleryApp
    
    Queues --&gt; Consumer
    Pool --&gt; Backend
    
    Scheduler --&gt; AMQP
</code></pre>
<p><em>注：图片来源 deepwiki<sup>1</sup></em></p>

<table>
  <thead>
    <tr>
      <th>组件</th>
      <th>用途</th>
      <th> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Celery应用</td>
      <td>嵌入在用户代码，支持方便的提交任务到队列</td>
      <td> </td>
    </tr>
    <tr>
      <td>消息基础设施</td>
      <td>外部服务，负责 队列管理、消息传递</td>
      <td> </td>
    </tr>
    <tr>
      <td>Worker</td>
      <td>进程管理、任务执行协调、并发任务执行（多进程/线程/异步</td>
      <td> </td>
    </tr>
    <tr>
      <td>结果后端</td>
      <td><code class="language-plaintext highlighter-rouge">celery/backends/base.py</code></td>
      <td>结果持久化、状态跟踪</td>
    </tr>
    <tr>
      <td>Beat调度器</td>
      <td><code class="language-plaintext highlighter-rouge">celery/apps/beat.py</code></td>
      <td>周期性任务调度</td>
    </tr>
  </tbody>
</table>

<p>用 Celery Github<sup>2</sup>上的极简例子说明上述组件在系统中的位置。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>from celery import Celery

app = Celery('hello', broker='redis://localhost/0', backend='redis://localhost/0',)

@app.task
def hello():
    return 'hello world'
</code></pre></div></div>

<p>这里定义了</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">app</code>定义了使用 Redis 作为任务的<strong>消息基础设施</strong>和<strong>结果后端</strong></li>
  <li>任务代码<code class="language-plaintext highlighter-rouge">hello</code>：跟普通定义没有差别，仅增加<code class="language-plaintext highlighter-rouge">@app.task</code>装饰</li>
</ol>

<p>在实际使用时，执行<code class="language-plaintext highlighter-rouge">hello.delay</code>将任务提交到队列，即<strong>Celery应用</strong>。</p>

<p>然后通过<code class="language-plaintext highlighter-rouge">celery -A celery_app worker</code>的形式，启动<strong>Worker</strong>，<code class="language-plaintext highlighter-rouge">hello</code>方法的实际执行，也是在 Worker 里。</p>

<p>进一步的示例，我们放到下一篇笔记介绍。</p>

<h2 id="4-进一步思考简洁的架构">4. 进一步思考：简洁的架构</h2>

<p>在 Celery 里，任务的生命周期：</p>

<pre><code class="language-mermaid">sequenceDiagram
    participant Client
    participant App as Celery App
    participant AMQP as AMQP Layer
    participant Broker
    participant Consumer
    participant Pool
    participant Backend
    
    Note over Client,Backend: 任务提交
    Client-&gt;&gt;App: task.apply_async(args, kwargs)
    App-&gt;&gt;App: 生成task_id (UUID)
    App-&gt;&gt;AMQP: create_task_message()
    AMQP-&gt;&gt;AMQP: 序列化args/kwargs/options
    AMQP-&gt;&gt;Broker: 发布到队列
    
    Note over Client,Backend: 任务执行
    Broker-&gt;&gt;Consumer: 投递消息
    Consumer-&gt;&gt;Consumer: 反序列化消息
    Consumer-&gt;&gt;Consumer: 确认消息
    Consumer-&gt;&gt;Pool: 提交到工作池
    Pool-&gt;&gt;Pool: 执行task.run()
    Pool-&gt;&gt;Backend: store_result(task_id, result)
    
    Note over Client,Backend: 结果检索
    Client-&gt;&gt;Backend: result.get(task_id)
    Backend-&gt;&gt;Client: 返回结果或抛出异常
</code></pre>
<p><em>注：图片来源 deepwiki<sup>1</sup></em></p>

<p>从任务的生命周期看，各个阶段参与的角色、职责非常清晰。</p>

<p>开始使用 Celery，用来对比的是大数据的任务调度系统，比如 Airflow DolphinScheduler 等，而 Celery 做到了<strong>真正的去中心化</strong>。我觉得核心原因在于周期任务，如果考虑任务调度，就需要引入 master 角色来实现准时且唯一的调度能力，而 Celery 的定位是任务队列，天然就避免了这一层。</p>

<p>当 Celery 通过 Celery Beat 支持周期任务后，这个问题就再次暴露出来，结果就是 Celery Beat 的单点风险。</p>

<p>同时通过任务生命周期可以看到，任务提交是在<code class="language-plaintext highlighter-rouge">Client</code>，而执行是在<code class="language-plaintext highlighter-rouge">Pool</code>，也就意味着：</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">Client</code>至少需要看到 task 的声明</li>
  <li><code class="language-plaintext highlighter-rouge">Pool</code>需要看到 task 的定义</li>
</ol>

<p>这个区别的影响在下一篇的例子里也会进一步说明。</p>

<p>这种使用方式让我想到 RPC 里 Client Server 的关系，比如 protobuffer 以及 proto 定义，两者都会用到，因此 RPC 的消息可以仅包含各类方法、实体名字即可，通过 PB 的兼容性确保了处理不会因为升级出错。</p>

<p>对应的经验，就是要<strong>注意升级任务定义的兼容性</strong>。比如<code class="language-plaintext highlighter-rouge">Client</code>里提交的 task 定义是<code class="language-plaintext highlighter-rouge">def task(file_path: str, type: int)</code>，而<code class="language-plaintext highlighter-rouge">Pool</code>执行的 task 在升级时变成了<code class="language-plaintext highlighter-rouge">def task(file_content: str, type: str)</code>，就会有问题，需要避免。</p>

<h2 id="5-参考资料">5. 参考资料</h2>

<ol>
  <li><a href="https://deepwiki.com/celery/celery">deepwiki</a></li>
  <li><a href="https://github.com/celery/celery">celery</a></li>
</ol>]]></content><author><name>ying</name></author><category term="microservice" /><summary type="html"><![CDATA[1. 定位：Celery 是什么？ Celery 是一个分布式任务队列系统，用于在多个工作进程和机器之间异步执行任务。 异步任务的需求很多，特别是耗时且需要平滑处理的场景。比如文件格式转换、数据统计、调用第三方耗时的 api 等，就需要任务入队，然后逐批出队处理。 有些情况下，异步任务还往往伴随着延迟或者周期处理的需求，例如统计网站使用量、文件个数大小等，Celery 也还支持了周期及延迟任务。 2. 思考：如果自己实现 实现任务队列，基础是链条上参与的三个角色：生产者、队列、消费者 首先是生产者：需要支持用户自定义任务，能够方便的提交到任务队列，队列里存储的应当是任务的元信息。这块需要提供代码框架或者接口实现。 其次是队列：任务不丢的基础是持久化。我的第一个想法是采用消息队列存储，Celery 则支持了多种存储格式。 最后是消费者：从队列中取出任务，实际执行用户任务代码。这里简单易用的实现方式是线程池，如果用户代码不可控、或者环境冲突，任务就需要放到容器执行。 基于上述基础功能，我们可能还要考虑很多实际生产问题： 是否需要背压：类似流式管道，队列是否是有界的，当生产速度已经远远大于消费速度，是否应当让生产者感知？ 不丢：比如消费者取走任务后，队列服务、消费者服务重启，此时是否会导致丢任务？ 不重：任务不能重复分发，如果是分布式系统下的典型 Unknown 场景，那此时是保证了 Exactly-once 还是 At-least-once ？大部分场景下我们追求后者，转而要求用户任务应当是幂等的。 失败重试：任务队列是否支持重试任务，还是让任务自身重试？如果任务自身，在任务队列系统看来跟普通执行并没有区别，可能会一直占着并发 延迟处理：任务队列能否指定该异步任务半小时后执行？ 运行结果：如果是 fire and forget 的任务，不需要结果，可是如果是需要任务运行后的结果呢？类似于 akka 的 ? ! 监控：当前有多少任务执行？多少排队？ 3. 架构：Celery 怎么实现的 graph TB subgraph "客户端层" ClientApp["客户端应用"] TaskDef["任务定义&lt;br/&gt;@app.task装饰器"] end subgraph "Celery应用" CeleryApp["Celery&lt;br/&gt;celery/app/base.py"] TaskRegistry["任务注册表&lt;br/&gt;celery/app/registry.py"] ConfigSystem["配置系统&lt;br/&gt;celery/app/defaults.py"] AMQP["AMQP层&lt;br/&gt;celery/app/amqp.py"] CeleryApp --&gt; TaskRegistry CeleryApp --&gt; ConfigSystem CeleryApp --&gt; AMQP end subgraph "消息基础设施" Broker["消息代理&lt;br/&gt;RabbitMQ/Redis/SQS"] Queues["任务队列"] Router["消息路由器&lt;br/&gt;celery.app.routes"] Broker --&gt; Queues AMQP --&gt; Router Router --&gt; Broker end subgraph "Worker系统" WorkerApp["Worker&lt;br/&gt;celery/apps/worker.py"] Consumer["消费者&lt;br/&gt;celery/worker/consumer.py"] Pool["执行池&lt;br/&gt;prefork/eventlet/gevent"] WorkerApp --&gt; Consumer Consumer --&gt; Pool end subgraph "结果存储" Backend["结果后端&lt;br/&gt;celery/backends/base.py"] RedisBackend["Redis后端"] RPCBackend["RPC后端"] DatabaseBackend["数据库后端"] Backend --&gt; RedisBackend Backend --&gt; RPCBackend Backend --&gt; DatabaseBackend end subgraph "调度器系统" BeatApp["Beat&lt;br/&gt;celery/apps/beat.py"] Scheduler["调度器&lt;br/&gt;celery/beat.py"] BeatApp --&gt; Scheduler end ClientApp --&gt; TaskDef TaskDef --&gt; CeleryApp Queues --&gt; Consumer Pool --&gt; Backend Scheduler --&gt; AMQP 注：图片来源 deepwiki1 组件 用途   Celery应用 嵌入在用户代码，支持方便的提交任务到队列   消息基础设施 外部服务，负责 队列管理、消息传递   Worker 进程管理、任务执行协调、并发任务执行（多进程/线程/异步   结果后端 celery/backends/base.py 结果持久化、状态跟踪 Beat调度器 celery/apps/beat.py 周期性任务调度 用 Celery Github2上的极简例子说明上述组件在系统中的位置。 from celery import Celery app = Celery('hello', broker='redis://localhost/0', backend='redis://localhost/0',) @app.task def hello(): return 'hello world' 这里定义了 app定义了使用 Redis 作为任务的消息基础设施和结果后端 任务代码hello：跟普通定义没有差别，仅增加@app.task装饰 在实际使用时，执行hello.delay将任务提交到队列，即Celery应用。 然后通过celery -A celery_app worker的形式，启动Worker，hello方法的实际执行，也是在 Worker 里。 进一步的示例，我们放到下一篇笔记介绍。 4. 进一步思考：简洁的架构 在 Celery 里，任务的生命周期： sequenceDiagram participant Client participant App as Celery App participant AMQP as AMQP Layer participant Broker participant Consumer participant Pool participant Backend Note over Client,Backend: 任务提交 Client-&gt;&gt;App: task.apply_async(args, kwargs) App-&gt;&gt;App: 生成task_id (UUID) App-&gt;&gt;AMQP: create_task_message() AMQP-&gt;&gt;AMQP: 序列化args/kwargs/options AMQP-&gt;&gt;Broker: 发布到队列 Note over Client,Backend: 任务执行 Broker-&gt;&gt;Consumer: 投递消息 Consumer-&gt;&gt;Consumer: 反序列化消息 Consumer-&gt;&gt;Consumer: 确认消息 Consumer-&gt;&gt;Pool: 提交到工作池 Pool-&gt;&gt;Pool: 执行task.run() Pool-&gt;&gt;Backend: store_result(task_id, result) Note over Client,Backend: 结果检索 Client-&gt;&gt;Backend: result.get(task_id) Backend-&gt;&gt;Client: 返回结果或抛出异常 注：图片来源 deepwiki1 从任务的生命周期看，各个阶段参与的角色、职责非常清晰。 开始使用 Celery，用来对比的是大数据的任务调度系统，比如 Airflow DolphinScheduler 等，而 Celery 做到了真正的去中心化。我觉得核心原因在于周期任务，如果考虑任务调度，就需要引入 master 角色来实现准时且唯一的调度能力，而 Celery 的定位是任务队列，天然就避免了这一层。 当 Celery 通过 Celery Beat 支持周期任务后，这个问题就再次暴露出来，结果就是 Celery Beat 的单点风险。 同时通过任务生命周期可以看到，任务提交是在Client，而执行是在Pool，也就意味着： Client至少需要看到 task 的声明 Pool需要看到 task 的定义 这个区别的影响在下一篇的例子里也会进一步说明。 这种使用方式让我想到 RPC 里 Client Server 的关系，比如 protobuffer 以及 proto 定义，两者都会用到，因此 RPC 的消息可以仅包含各类方法、实体名字即可，通过 PB 的兼容性确保了处理不会因为升级出错。 对应的经验，就是要注意升级任务定义的兼容性。比如Client里提交的 task 定义是def task(file_path: str, type: int)，而Pool执行的 task 在升级时变成了def task(file_content: str, type: str)，就会有问题，需要避免。 5. 参考资料 deepwiki celery]]></summary></entry><entry><title type="html">2025年个人总结</title><link href="https://izualzhy.cn/2025-summary" rel="alternate" type="text/html" title="2025年个人总结" /><published>2026-01-02T03:26:49+00:00</published><updated>2026-01-02T03:26:49+00:00</updated><id>https://izualzhy.cn/2025-summary</id><content type="html" xml:base="https://izualzhy.cn/2025-summary"><![CDATA[<h2 id="1-工作">1. 工作</h2>

<p>工作节奏相比互联网变平缓了。有时也会遇到难题，不过组内氛围很好，大家充分发挥“三个臭皮匠顶个诸葛亮”的精神，一块硬着头皮解决。</p>

<p>不忙的时候，有几次一路蹬着自行车回家，看着主路上车水马龙、辅路上人来人往，川流不息，恍惚间有种刚来北京读书时对这个城市的感觉。</p>

<p>工作内容偏功能性一些，性能、稳定性、辅助系统很少涉及。今年要随着项目开展，多研究些开源项目，多去思考生产落地的实际问题。</p>

<h2 id="2-读书">2. 读书</h2>

<p>2025 读的书不多，AI 相关耗时最久。今年想在此基础上，读更多关于历史的书。</p>

<table>
  <thead>
    <tr>
      <th>书名</th>
      <th>一句话总结</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>《人工智能工程化：应用落地与中台构建》</td>
      <td>第一次一窥其他人眼中 AI 中台的全貌，当前 AI 的基建、中台、应用都在路上，枝干结实了，才能结出丰硕的果实。今年还需要继续跟进关于 AI 中台、基建的书籍和文章。</td>
    </tr>
    <tr>
      <td>《大模型应用开发 动手做AI Agent》</td>
      <td>代码入门，以及简单的论文介绍</td>
    </tr>
    <tr>
      <td>《AI Agent开发与应用：基于大模型的智能体构建》</td>
      <td>像是搬过来了各种 User Guide 的 Chapter-1，而且过时。推荐直接看官方文档，及时且准确。</td>
    </tr>
    <tr>
      <td>《图解大模型-生成式AI原理与实战》</td>
      <td>系统性的了解AI，豆瓣只有7.6分，不过非常适合我现在的知识存储和眼界，推荐。</td>
    </tr>
    <tr>
      <td>《漫画百年党史•开天辟地》</td>
      <td>上下班路上看完，混子哥的书，怎么说呢，看得过瘾记住的不多。知识果然没有捷径。</td>
    </tr>
    <tr>
      <td>《以日为鉴》</td>
      <td>对事实有共鸣，对原因有疑问</td>
    </tr>
    <tr>
      <td>《为什么不能多印点钱》</td>
      <td>系统性的了解经济学，而且不枯燥</td>
    </tr>
    <tr>
      <td>《我是你爸爸》</td>
      <td>90 年代写成的书，现在看一点都不过时。我觉得现在都在感慨的工作危机、青春迷茫、压力在各个阶段都存在，只是我们突然有了共鸣而已。社会是个不变的舞台，变化的只是不断上场的人。台上的人，感触大抵都是相似的。推荐。</td>
    </tr>
    <tr>
      <td>《长安的荔枝》</td>
      <td>这几年陆陆续续看了几本马亲王的书。第一次知道作者是在九州奇幻里，感叹星云变幻、物是人非。推荐。</td>
    </tr>
  </tbody>
</table>

<h2 id="3-生活">3. 生活</h2>

<p>假期去了杭州、上海、厦门，祖国大好河山，当真需要“读万卷书，行万里路”。</p>

<p>平时能够关注到孩子的学习了，能够将自己的学习经验一点点的言传身教，看着她慢慢的领会，同时试着用自己的感悟跟你辩论，是件不错的事情。不过原来我总感叹大学学的都还给老师了，现在怀疑小学的可能也还了一些。</p>

<p>体重控制的目标达成，体检时医生说最好再瘦个三五斤，力量有所增加，还要继续努力💪。</p>

<p>2026，希望能到更多的地方看看。</p>]]></content><author><name>ying</name></author><category term="Patronum" /><summary type="html"><![CDATA[1. 工作 工作节奏相比互联网变平缓了。有时也会遇到难题，不过组内氛围很好，大家充分发挥“三个臭皮匠顶个诸葛亮”的精神，一块硬着头皮解决。 不忙的时候，有几次一路蹬着自行车回家，看着主路上车水马龙、辅路上人来人往，川流不息，恍惚间有种刚来北京读书时对这个城市的感觉。 工作内容偏功能性一些，性能、稳定性、辅助系统很少涉及。今年要随着项目开展，多研究些开源项目，多去思考生产落地的实际问题。 2. 读书 2025 读的书不多，AI 相关耗时最久。今年想在此基础上，读更多关于历史的书。 书名 一句话总结 《人工智能工程化：应用落地与中台构建》 第一次一窥其他人眼中 AI 中台的全貌，当前 AI 的基建、中台、应用都在路上，枝干结实了，才能结出丰硕的果实。今年还需要继续跟进关于 AI 中台、基建的书籍和文章。 《大模型应用开发 动手做AI Agent》 代码入门，以及简单的论文介绍 《AI Agent开发与应用：基于大模型的智能体构建》 像是搬过来了各种 User Guide 的 Chapter-1，而且过时。推荐直接看官方文档，及时且准确。 《图解大模型-生成式AI原理与实战》 系统性的了解AI，豆瓣只有7.6分，不过非常适合我现在的知识存储和眼界，推荐。 《漫画百年党史•开天辟地》 上下班路上看完，混子哥的书，怎么说呢，看得过瘾记住的不多。知识果然没有捷径。 《以日为鉴》 对事实有共鸣，对原因有疑问 《为什么不能多印点钱》 系统性的了解经济学，而且不枯燥 《我是你爸爸》 90 年代写成的书，现在看一点都不过时。我觉得现在都在感慨的工作危机、青春迷茫、压力在各个阶段都存在，只是我们突然有了共鸣而已。社会是个不变的舞台，变化的只是不断上场的人。台上的人，感触大抵都是相似的。推荐。 《长安的荔枝》 这几年陆陆续续看了几本马亲王的书。第一次知道作者是在九州奇幻里，感叹星云变幻、物是人非。推荐。 3. 生活 假期去了杭州、上海、厦门，祖国大好河山，当真需要“读万卷书，行万里路”。 平时能够关注到孩子的学习了，能够将自己的学习经验一点点的言传身教，看着她慢慢的领会，同时试着用自己的感悟跟你辩论，是件不错的事情。不过原来我总感叹大学学的都还给老师了，现在怀疑小学的可能也还了一些。 体重控制的目标达成，体检时医生说最好再瘦个三五斤，力量有所增加，还要继续努力💪。 2026，希望能到更多的地方看看。]]></summary></entry><entry><title type="html">自我实现的预言-读《为什么不能多印点钱》</title><link href="https://izualzhy.cn/wsmbndydq-reading" rel="alternate" type="text/html" title="自我实现的预言-读《为什么不能多印点钱》" /><published>2025-11-23T09:42:16+00:00</published><updated>2025-11-23T09:42:16+00:00</updated><id>https://izualzhy.cn/wsmbndydq-reading</id><content type="html" xml:base="https://izualzhy.cn/wsmbndydq-reading"><![CDATA[<p><img src="https://izualzhy.cn/assets/images/book/s35114855.jpg" alt="为什么不能多印点钱" /></p>

<h2 id="1-自我实现的预言">1. 自我实现的预言</h2>

<p>书中多次提到了这点，用更加通俗的话来说就是“信心”。对市场没有信心，普通民众自然就会捂紧了钱袋子；而随着消费变少，市场流动性变差，商铺利润变少甚至关门，市场自然就很难。</p>

<p>就像货币之所以有效，是因为我们相信它有效。</p>

<p>我觉得也像是一个自我驱动的循环。</p>

<h2 id="2-机会成本">2. 机会成本</h2>

<p>机会成本是因为选择这个物品或者服务而放弃其他选择的成本。比如同意加班意味着拒绝给女儿读睡前故事；同意在聚会时再喝一杯，就意味着失去了第二天的晨跑和清醒的头脑。</p>

<p>我们需要在得失间做一个选择，比如天天加班熬夜熬到精神抑郁、腰肌劳损，还是早点回家休息。听起来前者问题很大，但是选择了前者，也得到了更多的金钱。进一步的，周末是否值周、节假日是否能放心出去游玩、选择一个什么样的工作等等。</p>

<p>经济学意义上，所有的成本都是机会成本。</p>

<p>带给我的思考，就是从不同的角度重新思考选择，当决定做某件事时，不要考虑你在对什么说“是”，也要考虑在对什么说“不”。</p>

<h2 id="3-各司其职">3. 各司其职</h2>

<p>市场促进了人们各司其职，有人种小麦、有人开面粉厂、有人做面包、有人卖面包。</p>

<p>专注于某一件事情使得利润最大化，个人的成本变小，但是风险变高了。全球供应链，使得每个环节都受到上下游、政治、环境等的影响。对应到工厂里则每个人都变成了螺丝钉。</p>

<p>硬币的另一面，就是个人越来越不重要，就像蜂巢里的一只蜜蜂🐝</p>

<h2 id="4-工作">4. 工作</h2>

<p>公司成立是为了解决市场需求。</p>

<p>你这个人是公司需求的衍生品，公司需要的是你生产的商品。除非你的代码是公司的商品，否则对于工作上的架构或者代码，都不要太过执着。</p>

<p>所谓的核心价值/壁垒/护城河/不可替代性，普通人很难找到，很多公司找了多年了都没找到，所以尽力就行，但是不能放弃寻找。</p>

<h2 id="5-相比祖辈我们变富有了">5. 相比祖辈，我们变富有了</h2>

<p>这是地球上大部分人的感觉，因为整个蛋糕变得更大了。</p>

<h2 id="6-gdp">6. GDP</h2>

<p>如何判断蛋糕真的变大了？经济体系是巨大的，由无数人和他们的无数选择组成。因此，监测经济活动中发生的一切是一项艰巨的任务。</p>

<p>美国经济大萧条时期着手解决这个问题，国会请来了库兹涅茨。库兹涅茨和他的团队被要求找到一种衡量美国经济健康状况的方法。这是一个很高的要求，尤其是在一场前所未有的灾难性大萧条中。</p>

<p>库兹涅茨的团队迎难而上，提出了当今世界上通用的衡量经济健康状况的指标：国内生产总值(GDP)。</p>

<p>一个例子可以说明 GDP 的漏洞：假设你和邻居达成了协议，他们花20英镑请你打扫他们的客厅，而你也花20英镑请他们打扫你的客厅。一天下来，你们双方都既没有赚到钱，也没有经济损失，但你们的客厅都被打扫干净了。</p>

<p>但是这并不是用来否定 GDP 的，我的感觉反而是库兹涅茨的伟大之处，首先他承受住了当时的巨大压力，其次他提出了有用的指标。或许这个指标不完美，但是一是有用的，二是一直多年以后也没有人能够提出更优的指标。</p>

<h2 id="7-日光之下无新事">7. 日光之下无新事</h2>

<blockquote>
  <p>20世纪90年代末，收藏家开始相信某些豆豆宝宝是稀有的，因此会升值。他们说得对，至少有一段时间是这样。在这个时期，有些豆豆宝宝在易趣上的售价可以超过5000美元。</p>
</blockquote>

<p>类似现在追捧各类玩偶、卡牌等等</p>

<blockquote>
  <p>2005年初，欧盟实施了年度贸易配额，限制了一些中国商品的进口量。但中国政府认为这是不公平的，于是进行了有力的反击。</p>
</blockquote>

<h2 id="8-总结">8. 总结</h2>

<p>不完美但是有用的模型：所有的经济模型都是不完美的，但是有效就行。</p>

<p>正如超现实主义画家萨尔瓦多·达利所说，你应该“不要在意是否完美，反正你永远也无法达到”。</p>

<p>工作里我们也应当铭记这点，追求完美往往意味着极度内卷，用极大的力量去完成最后 5%的冲刺，或许未来适合，但是在当前的阶段，我觉得已经不适合了。</p>]]></content><author><name>ying</name></author><category term="read" /><summary type="html"><![CDATA[1. 自我实现的预言 书中多次提到了这点，用更加通俗的话来说就是“信心”。对市场没有信心，普通民众自然就会捂紧了钱袋子；而随着消费变少，市场流动性变差，商铺利润变少甚至关门，市场自然就很难。 就像货币之所以有效，是因为我们相信它有效。 我觉得也像是一个自我驱动的循环。 2. 机会成本 机会成本是因为选择这个物品或者服务而放弃其他选择的成本。比如同意加班意味着拒绝给女儿读睡前故事；同意在聚会时再喝一杯，就意味着失去了第二天的晨跑和清醒的头脑。 我们需要在得失间做一个选择，比如天天加班熬夜熬到精神抑郁、腰肌劳损，还是早点回家休息。听起来前者问题很大，但是选择了前者，也得到了更多的金钱。进一步的，周末是否值周、节假日是否能放心出去游玩、选择一个什么样的工作等等。 经济学意义上，所有的成本都是机会成本。 带给我的思考，就是从不同的角度重新思考选择，当决定做某件事时，不要考虑你在对什么说“是”，也要考虑在对什么说“不”。 3. 各司其职 市场促进了人们各司其职，有人种小麦、有人开面粉厂、有人做面包、有人卖面包。 专注于某一件事情使得利润最大化，个人的成本变小，但是风险变高了。全球供应链，使得每个环节都受到上下游、政治、环境等的影响。对应到工厂里则每个人都变成了螺丝钉。 硬币的另一面，就是个人越来越不重要，就像蜂巢里的一只蜜蜂🐝 4. 工作 公司成立是为了解决市场需求。 你这个人是公司需求的衍生品，公司需要的是你生产的商品。除非你的代码是公司的商品，否则对于工作上的架构或者代码，都不要太过执着。 所谓的核心价值/壁垒/护城河/不可替代性，普通人很难找到，很多公司找了多年了都没找到，所以尽力就行，但是不能放弃寻找。 5. 相比祖辈，我们变富有了 这是地球上大部分人的感觉，因为整个蛋糕变得更大了。 6. GDP 如何判断蛋糕真的变大了？经济体系是巨大的，由无数人和他们的无数选择组成。因此，监测经济活动中发生的一切是一项艰巨的任务。 美国经济大萧条时期着手解决这个问题，国会请来了库兹涅茨。库兹涅茨和他的团队被要求找到一种衡量美国经济健康状况的方法。这是一个很高的要求，尤其是在一场前所未有的灾难性大萧条中。 库兹涅茨的团队迎难而上，提出了当今世界上通用的衡量经济健康状况的指标：国内生产总值(GDP)。 一个例子可以说明 GDP 的漏洞：假设你和邻居达成了协议，他们花20英镑请你打扫他们的客厅，而你也花20英镑请他们打扫你的客厅。一天下来，你们双方都既没有赚到钱，也没有经济损失，但你们的客厅都被打扫干净了。 但是这并不是用来否定 GDP 的，我的感觉反而是库兹涅茨的伟大之处，首先他承受住了当时的巨大压力，其次他提出了有用的指标。或许这个指标不完美，但是一是有用的，二是一直多年以后也没有人能够提出更优的指标。 7. 日光之下无新事 20世纪90年代末，收藏家开始相信某些豆豆宝宝是稀有的，因此会升值。他们说得对，至少有一段时间是这样。在这个时期，有些豆豆宝宝在易趣上的售价可以超过5000美元。 类似现在追捧各类玩偶、卡牌等等 2005年初，欧盟实施了年度贸易配额，限制了一些中国商品的进口量。但中国政府认为这是不公平的，于是进行了有力的反击。 8. 总结 不完美但是有用的模型：所有的经济模型都是不完美的，但是有效就行。 正如超现实主义画家萨尔瓦多·达利所说，你应该“不要在意是否完美，反正你永远也无法达到”。 工作里我们也应当铭记这点，追求完美往往意味着极度内卷，用极大的力量去完成最后 5%的冲刺，或许未来适合，但是在当前的阶段，我觉得已经不适合了。]]></summary></entry><entry><title type="html">记忆和向量-读《图解大模型》</title><link href="https://izualzhy.cn/hands-on-LLM-reading" rel="alternate" type="text/html" title="记忆和向量-读《图解大模型》" /><published>2025-09-27T09:05:26+00:00</published><updated>2025-09-27T09:05:26+00:00</updated><id>https://izualzhy.cn/hands-on-LLM-reading</id><content type="html" xml:base="https://izualzhy.cn/hands-on-LLM-reading"><![CDATA[<h2 id="1-记忆">1. 记忆</h2>

<p>大模型是无状态，不会记住任何先前的对话内容。</p>

<p><img src="/assets/images/hands-on-llm/memory.jpg" alt="" width="500" /></p>

<p><em>注意书里介绍的是短期记忆</em></p>

<p>常见记忆方式有两种：</p>
<ol>
  <li>对话缓冲区</li>
  <li>对话摘要</li>
</ol>

<h3 id="11-对话缓冲区">1.1. 对话缓冲区</h3>

<p><img src="/assets/images/hands-on-llm/conversation_buffer_memory.jpg" alt="" width="500" /></p>

<p>LangChain 框架使用<code class="language-plaintext highlighter-rouge">ConversationBufferMemory</code> <code class="language-plaintext highlighter-rouge">ConversationBufferWindowMemory</code>实现，原理上就是将每轮对话的<code class="language-plaintext highlighter-rouge">user</code> <code class="language-plaintext highlighter-rouge">system</code>都发给大模型，上限则通过对话轮数或者 tokens 个数控制。</p>

<h3 id="12-对话摘要">1.2. 对话摘要</h3>

<p>如果不控制上限，对话缓冲区随着对话内容持续增长，并逐渐逼近模型的词元限制；如果控制上限，又可能丢失较早的对话内容。</p>

<p>对话摘要用来解决这个问题。</p>

<p><img src="/assets/images/hands-on-llm/prompt_summary_memory.jpg" alt="" width="500" /></p>

<p>即将历史对话转为摘要。提取摘要的过程，依赖 LLM 参与，例如可能的提示词：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 创建摘要提示词模板
</span><span class="n">summary_prompt_template</span> <span class="o">=</span> <span class="s">"""&lt;s&gt;&lt;|user|&gt;Summarize the conversations and update
with the new lines.
Current summary:
{summary}
new lines of conversation:
{new_lines}
New summary:&lt;|end|&gt;
&lt;|assistant|&gt;"""</span>
<span class="n">summary_prompt</span> <span class="o">=</span> <span class="n">PromptTemplate</span><span class="p">(</span>
    <span class="n">input_variables</span><span class="o">=</span><span class="p">[</span><span class="s">"new_lines"</span><span class="p">,</span> <span class="s">"summary"</span><span class="p">],</span>
    <span class="n">template</span><span class="o">=</span><span class="n">summary_prompt_template</span>
<span class="p">)</span>
</code></pre></div></div>

<p>输入是<code class="language-plaintext highlighter-rouge">Nth 轮之前的 summary、N 轮之后的对话</code>，输出则是<code class="language-plaintext highlighter-rouge">新的 summary</code></p>

<p><img src="/assets/images/hands-on-llm/llm_with_summary_memory.jpg" alt="" width="500" /></p>

<h3 id="13-记忆总结">1.3. 记忆总结</h3>

<p><img src="/assets/images/hands-on-llm/compare_memory.jpg" alt="" width="500" /></p>

<p>实际工程里用到的记忆框架，例如 Mem0、MemOS 等，要复杂的多，但是我觉得本质还是一回事，区别是给自己扩大了 scope.</p>

<h2 id="2-rag">2. RAG</h2>

<p>通过 RAG 里的知识库，解决了 LLM 知识过时、没有专业知识的问题。</p>

<p><img src="/assets/images/hands-on-llm/rag.jpg" alt="" width="500" /></p>

<h3 id="21-向量化">2.1. 向量化</h3>

<p><img src="/assets/images/hands-on-llm/knowledge_embedding.jpg" alt="" width="500" /></p>

<ol>
  <li>文档分块</li>
  <li>各分块通过嵌入模型转化为向量表示</li>
  <li>存储在向量数据库中以备检索</li>
</ol>

<h3 id="22-长文本分块策略">2.2. 长文本分块策略</h3>

<p>这里一个很大的挑战是 LLM 有 tokens 上限。</p>

<p>有两个解决思路：</p>
<ol>
  <li>单文档单向量</li>
  <li>单文档多向量</li>
</ol>

<p><img src="/assets/images/hands-on-llm/single_multi_doc.jpg" alt="" width="500" /></p>

<p>单文档单向量，需要能够提取出代表性的段落，忽略剩余内容。例如标题、文章前几段等。这里让我想起来构建搜索系统时，spider 系统抓取到网页后，需要能够基于模型提取标题、过滤低质的广告内容等。基于加工后的数据建库，才能起到好的搜索效果。</p>

<p>单文档多向量，则是把长文本分块，解决单个分块太大导致 LLM 无法处理或者丢失重点。</p>

<p>常见的分段方式有：</p>

<p><img src="/assets/images/hands-on-llm/multi_doc.jpg" alt="" width="500" /></p>

<p>目前看到的大部分智能体平台，都是丢给用户一个页面，让用户输入分隔符、分段长度、是否重叠等等，而这里对最后的召回效果影响极大，如何能够提早针对搜索效果，优化这个建库的过程，应该是非常值得做的方向。</p>

<p>实际应用到生产，很可能是上述两种思路的结合：</p>
<ol>
  <li>即使是单文档多向量，也需要先采用单文档提取的方式，过滤低质、提取重点，然后再经过不同方式分段。对每个分段，也保留文章的核心信息，这样才能避免分段后缺失整体文章重点的问题。</li>
  <li>作者的观点：“随着稠密检索技术的持续演进，更多创新的分块策略正在涌现——部分方案已开始利用LLM实现动态智能分块，以生成语义连贯的文本单元。”</li>
</ol>]]></content><author><name>ying</name></author><category term="book" /><category term="read" /><summary type="html"><![CDATA[1. 记忆 大模型是无状态，不会记住任何先前的对话内容。 注意书里介绍的是短期记忆 常见记忆方式有两种： 对话缓冲区 对话摘要 1.1. 对话缓冲区 LangChain 框架使用ConversationBufferMemory ConversationBufferWindowMemory实现，原理上就是将每轮对话的user system都发给大模型，上限则通过对话轮数或者 tokens 个数控制。 1.2. 对话摘要 如果不控制上限，对话缓冲区随着对话内容持续增长，并逐渐逼近模型的词元限制；如果控制上限，又可能丢失较早的对话内容。 对话摘要用来解决这个问题。 即将历史对话转为摘要。提取摘要的过程，依赖 LLM 参与，例如可能的提示词： # 创建摘要提示词模板 summary_prompt_template = """&lt;s&gt;&lt;|user|&gt;Summarize the conversations and update with the new lines. Current summary: {summary} new lines of conversation: {new_lines} New summary:&lt;|end|&gt; &lt;|assistant|&gt;""" summary_prompt = PromptTemplate( input_variables=["new_lines", "summary"], template=summary_prompt_template ) 输入是Nth 轮之前的 summary、N 轮之后的对话，输出则是新的 summary 1.3. 记忆总结 实际工程里用到的记忆框架，例如 Mem0、MemOS 等，要复杂的多，但是我觉得本质还是一回事，区别是给自己扩大了 scope. 2. RAG 通过 RAG 里的知识库，解决了 LLM 知识过时、没有专业知识的问题。 2.1. 向量化 文档分块 各分块通过嵌入模型转化为向量表示 存储在向量数据库中以备检索 2.2. 长文本分块策略 这里一个很大的挑战是 LLM 有 tokens 上限。 有两个解决思路： 单文档单向量 单文档多向量 单文档单向量，需要能够提取出代表性的段落，忽略剩余内容。例如标题、文章前几段等。这里让我想起来构建搜索系统时，spider 系统抓取到网页后，需要能够基于模型提取标题、过滤低质的广告内容等。基于加工后的数据建库，才能起到好的搜索效果。 单文档多向量，则是把长文本分块，解决单个分块太大导致 LLM 无法处理或者丢失重点。 常见的分段方式有： 目前看到的大部分智能体平台，都是丢给用户一个页面，让用户输入分隔符、分段长度、是否重叠等等，而这里对最后的召回效果影响极大，如何能够提早针对搜索效果，优化这个建库的过程，应该是非常值得做的方向。 实际应用到生产，很可能是上述两种思路的结合： 即使是单文档多向量，也需要先采用单文档提取的方式，过滤低质、提取重点，然后再经过不同方式分段。对每个分段，也保留文章的核心信息，这样才能避免分段后缺失整体文章重点的问题。 作者的观点：“随着稠密检索技术的持续演进，更多创新的分块策略正在涌现——部分方案已开始利用LLM实现动态智能分块，以生成语义连贯的文本单元。”]]></summary></entry><entry><title type="html">LLM Paper&amp;amp;Practice: CAMEL 和 AutoGen</title><link href="https://izualzhy.cn/llm-paper-read-camel-autogen" rel="alternate" type="text/html" title="LLM Paper&amp;amp;Practice: CAMEL 和 AutoGen" /><published>2025-07-12T12:31:21+00:00</published><updated>2025-07-12T12:31:21+00:00</updated><id>https://izualzhy.cn/llm-paper-read-camel-autogen</id><content type="html" xml:base="https://izualzhy.cn/llm-paper-read-camel-autogen"><![CDATA[<p>本文是我在阅读《大模型应用开发 动手做AI Agent》时，对书中推荐的几篇核心论文的理解与总结，主要聚焦于<strong>CAMEL</strong>和 <strong>AutoGen</strong> 这两种范式，都是用来实现 MultiAgent 的。</p>

<h1 id="1-camel">1. CAMEL</h1>

<h2 id="11-什么是-camel">1.1. 什么是 CAMEL?</h2>

<p>CAMEL 是 Communicative Agents for “Mind” Exploration of Large Language Model Society 的简称。</p>

<p>论文只比 ReAct 晚半年发表，提出了一种让智能体间互相协作的方法，中间过程无需人为干预。</p>

<h2 id="12-camel-的背景和想法">1.2. CAMEL 的背景和想法</h2>

<p>CAMEL 的研究团队，发现人们在使用 LLM 解决复杂任务时，还是非常依赖人力投入的。投入主要在于人们需要不断和智能体沟通，以确保对话朝着正确的方向发展。</p>

<p>那是否可以让智能体之间互相合作/监督？我感觉这里有点像是训练任务里对抗网络的意思。</p>

<p>CAMEL 的做法是让智能体之间对话，只需要最开始时人为设定任务、Prompt，之后就由智能体探索并给出答案。</p>

<p>为此，CAMEL 提出了 RolePlaying 的概念，没错，就是 RPG 游戏里的 RP😅</p>

<p>每个 Agent 都会遵守角色设定分工合作，不同 Agent 之间还是用自然语言通信。说白了，相比单 Agent 的变化就是：</p>
<ol>
  <li><strong>原来 Agent 的输出发给人，人为判断后给出新的输入。</strong></li>
  <li><strong>现在 Agent 的输出发给另一个 Agent 作为输入，反之亦然</strong></li>
</ol>

<p>思路上还是用 Agent 来替代人，如果发现不可替代，那就多搞几个 Agent ，还不行的话，或许就搞一个超级Agent(替代人做决策判断)。</p>

<p>扯远了，回到论文本身。论文还提出一种观点，Agent 之间可以用自然语言通信。</p>

<blockquote>
  <p>Communicative Agents. Communication between agents has been studied for a long time [76,77]. There are many ways to facilitate communication between agents, and with agents [29, 90,97]. Among these, natural language is considered the most natural form of communication [97].</p>
</blockquote>

<p><img src="/assets/images/ai-paper/camel_figure_1.png" alt="camel_figure_1" /></p>

<p>上图就是 CAMEL 思想的例子，左边是人为输入部分，设定两部分内容：</p>
<ol>
  <li>Idea: 需要 Agents 去探讨的想法、完成的任务等</li>
  <li>Role Assignment: 为不同的 Agent 设置不同的角色</li>
</ol>

<p>之后就是让右边的 Agent 不断交互了。</p>

<p>论文地址：<a href="https://arxiv.org/abs/2303.17760">CAMEL: Communicative Agents for “Mind” Exploration of Large Language Model Society</a></p>

<p>此外作者探索过程中，也发现了一些让 Agent 之间合作完成任务的难点，例如角色互换、复读机一样循环等问题：</p>

<blockquote>
  <p>Several challenges arise when asking a society of agents to autonomously cooperate on completing tasks. Examples we encountered in our preliminary analysis include role flipping, assistant repeating instructions, flake replies, and infinite loop of messages. Therefore, it is critical to investigate ways to align these models with human intentions and to explore means enabling their effective cooperation.</p>
</blockquote>

<p>这也是 RolePlaying 提出来的原因，每个智能体扮演好自己的角色，不要试图跨越。</p>

<p>所以论文的 Prompt 里会出现：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 助手 Prompt 示例
Never forget you are a &lt;ASSISTANT_ROLE&gt; and I am a &lt;USER_ROLE&gt;. 
Never flip roles!
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 用户 Prompt 示例
Never forget you are a &lt;USER_ROLE&gt; and I am a &lt;ASSISTANT_ROLE&gt;.
Never flip roles! 
</code></pre></div></div>

<h2 id="13-实践">1.3. 实践</h2>

<p>按照论文的思想，手写 Prompt 也可以实现。不过作者同时开源了<a href="https://docs.camel-ai.org/get_started/introduction">camel-ai/camel</a>框架，因此我们基于这个框架验证。</p>

<p>为了直观感受 CAMEL 的运作机制，我设计了一个“猜物品”游戏：</p>
<ul>
  <li><strong>回答者 (Responder)</strong>：知道目标物品（如“袋鼠”），但只能对问题回答“是/否/有/没有”等。</li>
  <li><strong>猜测者 (Guesser)</strong>：不知道物品，需要通过提问来猜出答案。</li>
</ul>

<h3 id="131-使用roleplaying-未调通">1.3.1 使用RolePlaying-未调通</h3>

<p>第一次验证我使用的是<a href="https://docs.camel-ai.org/cookbooks/multi_agent_society/agents_society#2-1-roleplaying">RolePlaying</a>. 但是实际效果很差，主要是两点：</p>

<ol>
  <li>由于是传入统一的<code class="language-plaintext highlighter-rouge">task_prompt</code>指定不同 role 的指令，因此猜物品的 Agent 其实知道是什么物品，就会按照这个方向问。比如我设定物品是袋鼠，Agent 就会问“哺乳动物”、“澳大利亚”这些词，感觉 Agent 只是为了执行你说的这个过程，而结果他早就知道了。</li>
  <li>由于 System Prompt 是固化的，所以返回值也是固化的，比如<code class="language-plaintext highlighter-rouge">Solution</code> <code class="language-plaintext highlighter-rouge">Instruction</code>这些</li>
</ol>

<p>具体代码：<a href="https://github.com/izualzhy/AI-Systems/blob/main/misc/camel_guess_item.py">https://github.com/izualzhy/AI-Systems/blob/main/misc/camel_guess_item.py</a></p>

<p>看实现，该类通过任务描述和 Agent 的名字，自动为每个 Agent 生成其角色指令。正如论文里的观点，代码里也定义了<code class="language-plaintext highlighter-rouge">assistant</code> <code class="language-plaintext highlighter-rouge">user</code>角色，例如都有哪几种角色：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># enums.py
</span><span class="k">class</span> <span class="nc">RoleType</span><span class="p">(</span><span class="n">Enum</span><span class="p">):</span>
    <span class="n">ASSISTANT</span> <span class="o">=</span> <span class="s">"assistant"</span>
    <span class="n">USER</span> <span class="o">=</span> <span class="s">"user"</span>
    <span class="n">CRITIC</span> <span class="o">=</span> <span class="s">"critic"</span>
    <span class="n">EMBODIMENT</span> <span class="o">=</span> <span class="s">"embodiment"</span>
    <span class="n">DEFAULT</span> <span class="o">=</span> <span class="s">"default"</span>
</code></pre></div></div>

<p>都有哪些 System Prompt</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># code.py
</span>    <span class="n">ASSISTANT_PROMPT</span> <span class="o">=</span> <span class="n">TextPrompt</span><span class="p">(</span>
        <span class="s">"""Never forget you are a Computer Programmer and I am a person working in {domain}. Never flip roles! Never instruct me!
We share a common interest in collaborating to successfully complete a task.
You must help me to complete the task using {language} programming language.
        ..."""</span>

<span class="c1"># ai_society.py
</span>    <span class="n">ASSISTANT_PROMPT</span><span class="p">:</span> <span class="n">TextPrompt</span> <span class="o">=</span> <span class="n">TextPrompt</span><span class="p">(</span><span class="s">"""===== RULES OF ASSISTANT =====
Never forget you are a {assistant_role} and I am a {user_role}. Never flip roles! Never instruct me!
We share a common interest in collaborating to successfully complete a task.
You must help me to complete the task.
        ..."""</span>
</code></pre></div></div>

<p>可以在<a href="https://github.com/camel-ai/camel/tree/master/camel/prompts">camel/camel/prompts</a>查看相关源码。</p>

<p>所以整个封装是比较重的，适合实现的场景也跟这些内置的 Prompt Role 有关。</p>

<p>虽然不符合预期，但也不是全无收获。在<code class="language-plaintext highlighter-rouge">RolePlaying</code>的实现里，看到了<code class="language-plaintext highlighter-rouge">ChatAgent</code>这个类：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RolePlaying</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">assistant_agent</span><span class="p">:</span> <span class="n">ChatAgent</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">user_agent</span><span class="p">:</span> <span class="n">ChatAgent</span>
</code></pre></div></div>
<p>于是我用<code class="language-plaintext highlighter-rouge">ChatAgent</code>进行了第二次测试。</p>

<h3 id="132-使用chatagent-符合预期">1.3.2 使用ChatAgent-符合预期</h3>

<p>了解了原理，也可以自己实现，比如《大模型应用开发 动手做AI Agent》里的例子:<a href="https://github.com/izualzhy/AI-Systems/blob/main/HandsOnAIAgent/camel_agent_demo.py">https://github.com/izualzhy/AI-Systems/blob/main/HandsOnAIAgent/camel_agent_demo.py</a>。</p>

<p>为了熟悉下 CAMEL 框架，我还是上一节看到的<code class="language-plaintext highlighter-rouge">ChatAgent</code>。</p>

<p>废话不多说，show code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python
# coding=utf-8
</span>
<span class="kn">import</span> <span class="nn">os</span>

<span class="kn">from</span> <span class="nn">camel.agents</span> <span class="kn">import</span> <span class="n">ChatAgent</span>
<span class="kn">from</span> <span class="nn">camel.configs</span> <span class="kn">import</span> <span class="n">ChatGPTConfig</span>
<span class="kn">from</span> <span class="nn">camel.models</span> <span class="kn">import</span> <span class="n">ModelFactory</span>
<span class="kn">from</span> <span class="nn">camel.types</span> <span class="kn">import</span> <span class="n">ModelPlatformType</span>
<span class="kn">from</span> <span class="nn">colorama</span> <span class="kn">import</span> <span class="n">Fore</span>

<span class="kn">from</span> <span class="nn">util</span> <span class="kn">import</span> <span class="n">DOUBAO_SEED_1_6</span><span class="p">,</span> <span class="n">ARK_API_URL</span>

<span class="c1"># 设置目标词
</span><span class="n">target_word</span> <span class="o">=</span> <span class="s">"袋鼠"</span>

<span class="n">doubao_config</span> <span class="o">=</span> <span class="n">ChatGPTConfig</span><span class="p">(</span>
    <span class="n">temperature</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
    <span class="n">top_p</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
    <span class="n">max_tokens</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
    <span class="n">stream</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
    <span class="n">stop</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">doubao_model</span> <span class="o">=</span> <span class="n">ModelFactory</span><span class="p">.</span><span class="n">create</span><span class="p">(</span>
    <span class="n">model_platform</span><span class="o">=</span><span class="n">ModelPlatformType</span><span class="p">.</span><span class="n">OPENAI_COMPATIBLE_MODEL</span><span class="p">,</span>
    <span class="n">model_type</span><span class="o">=</span><span class="n">DOUBAO_SEED_1_6</span><span class="p">,</span>
    <span class="n">model_config_dict</span><span class="o">=</span><span class="n">doubao_config</span><span class="p">.</span><span class="n">as_dict</span><span class="p">(),</span>
    <span class="n">url</span><span class="o">=</span><span class="n">ARK_API_URL</span><span class="p">,</span>
    <span class="n">api_key</span><span class="o">=</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">"ARK_API_KEY"</span><span class="p">]</span>
<span class="p">)</span>

<span class="c1"># 设置猜词者 system message（不能包含目标词）
</span><span class="n">guesser_sys_msg</span> <span class="o">=</span> <span class="s">"""
你将参与一个猜物品名字的游戏。你不知道物品是什么，需要通过不断提出‘是/不是’的问题来逐步猜出这个物品。
以下是游戏的规则：
- 每次只能提出一个‘是/不是’的问题。不要一次提多个问题。
- 当你认为你知道这个物品是什么时，请直接说出：‘这个词是___’。
- 只有当对方说“恭喜你猜对了！”，才说明你猜对了。
- 如果你的问题是“是/不是”类型，对方只回答“是”或“不是”或“有”或“没有”。
- 如果你说出的词是目标词的父类型，对方会回答：“是这个类型，但是词不对”
"""</span>

<span class="c1"># 设置回答者 system message（包含目标词）
</span><span class="n">responder_sys_msg</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"""
你正在参与一个猜物品名字的游戏，注意物品名字可能细化到品牌、分类等。你的任务是根据对方提出的“是/不是”问题，以特定方式回应，帮助对方猜出目标物品。
目标词是： </span><span class="si">{</span><span class="n">target_word</span><span class="si">}</span><span class="s">
对方会通过问“是/不是”问题来尝试猜出这个词。你只能按照以下规则回答：
- 如果对方的问题是“是/不是”类型，你只能回答“是”或“不是”或“有”或“没有”。
- 如果对方说出的词就是目标词，你要回答：“恭喜你猜对了！”
- 如果对方说出的词是目标词的父类型，你需要回答：“是这个类型，但是词不对”
- 除此之外不能泄露任何信息。
"""</span>

<span class="c1"># 创建两个 Agent
</span><span class="n">guesser</span> <span class="o">=</span> <span class="n">ChatAgent</span><span class="p">(</span><span class="n">system_message</span><span class="o">=</span><span class="n">guesser_sys_msg</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="n">doubao_model</span><span class="p">,</span> <span class="n">output_language</span><span class="o">=</span><span class="s">"zh-CN"</span><span class="p">)</span>
<span class="n">responder</span> <span class="o">=</span> <span class="n">ChatAgent</span><span class="p">(</span><span class="n">system_message</span><span class="o">=</span><span class="n">responder_sys_msg</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="n">doubao_model</span><span class="p">,</span> <span class="n">output_language</span><span class="o">=</span><span class="s">"zh-CN"</span><span class="p">)</span>

<span class="c1"># 初始化对话
</span><span class="n">guesser</span><span class="p">.</span><span class="n">reset</span><span class="p">()</span>
<span class="n">responder</span><span class="p">.</span><span class="n">reset</span><span class="p">()</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"开始游戏：</span><span class="si">{</span><span class="n">guesser</span><span class="si">}</span><span class="s"> 和 </span><span class="si">{</span><span class="n">responder</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">guesser</span><span class="si">}</span><span class="s"> 的系统信息：</span><span class="si">{</span><span class="n">guesser</span><span class="p">.</span><span class="n">role_name</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">guesser</span><span class="p">.</span><span class="n">role_type</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">guesser</span><span class="p">.</span><span class="n">chat_history</span><span class="si">}</span><span class="s"> "</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">responder</span><span class="si">}</span><span class="s"> 的系统信息：</span><span class="si">{</span><span class="n">responder</span><span class="p">.</span><span class="n">role_name</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">responder</span><span class="p">.</span><span class="n">role_type</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">responder</span><span class="p">.</span><span class="n">chat_history</span><span class="si">}</span><span class="s"> "</span><span class="p">)</span>

<span class="c1"># 启动对话循环
</span><span class="n">responder_reply</span> <span class="o">=</span> <span class="bp">None</span>

<span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">100</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="n">Fore</span><span class="p">.</span><span class="n">BLUE</span> <span class="o">+</span> <span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">🌀 第 </span><span class="si">{</span><span class="n">step</span><span class="si">}</span><span class="s"> 轮对话"</span><span class="p">)</span>

    <span class="c1"># 猜词者提问
</span>    <span class="k">if</span> <span class="n">step</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">guesser_reply</span> <span class="o">=</span> <span class="n">guesser</span><span class="p">.</span><span class="n">step</span><span class="p">(</span><span class="s">"开始"</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">guesser_reply</span> <span class="o">=</span> <span class="n">guesser</span><span class="p">.</span><span class="n">step</span><span class="p">(</span><span class="n">responder_reply</span><span class="p">.</span><span class="n">msg</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">Fore</span><span class="p">.</span><span class="n">YELLOW</span> <span class="o">+</span> <span class="sa">f</span><span class="s">"猜词者：</span><span class="si">{</span><span class="n">guesser_reply</span><span class="p">.</span><span class="n">msg</span><span class="p">.</span><span class="n">content</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">Fore</span><span class="p">.</span><span class="n">CYAN</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">guesser_reply</span><span class="p">.</span><span class="n">info</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'usage'</span><span class="p">)))</span>
    <span class="c1"># print(f"{guesser} 的历史信息：{guesser.chat_history} ")
</span>    <span class="c1"># print(f"{guesser} 的记忆：{guesser.memory.get_context()}")
</span>
    <span class="c1"># 回答者回应
</span>    <span class="n">responder_reply</span> <span class="o">=</span> <span class="n">responder</span><span class="p">.</span><span class="n">step</span><span class="p">(</span><span class="n">guesser_reply</span><span class="p">.</span><span class="n">msg</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">Fore</span><span class="p">.</span><span class="n">GREEN</span> <span class="o">+</span> <span class="sa">f</span><span class="s">"回答者：</span><span class="si">{</span><span class="n">responder_reply</span><span class="p">.</span><span class="n">msg</span><span class="p">.</span><span class="n">content</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">Fore</span><span class="p">.</span><span class="n">CYAN</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">responder_reply</span><span class="p">.</span><span class="n">info</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'usage'</span><span class="p">)))</span>
    <span class="c1"># print(f"{responder} 的历史信息：{responder.chat_history} ")
</span>    <span class="c1"># print(f"{responder} 的记忆：{responder.memory.get_context()}")
</span>
    <span class="c1"># 判断是否猜对
</span>    <span class="k">if</span> <span class="s">"恭喜你猜对了"</span> <span class="ow">in</span> <span class="n">responder_reply</span><span class="p">.</span><span class="n">msg</span><span class="p">.</span><span class="n">content</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">🎉 游戏结束：猜词成功！"</span><span class="p">)</span>
        <span class="k">break</span>

    <span class="c1"># 更新对话输入
</span>    <span class="n">responder_msg</span> <span class="o">=</span> <span class="n">responder_reply</span><span class="p">.</span><span class="n">msg</span>

</code></pre></div></div>

<p>分别创建两个<code class="language-plaintext highlighter-rouge">ChatAgent</code>，指定<code class="language-plaintext highlighter-rouge">system_message</code>，</p>

<p>可以看到执行效果上完全符合预期：</p>

<p><img src="/assets/images/ai-paper/camel_example_head.png" alt="camel_example_head" />
继续。。。
<img src="/assets/images/ai-paper/camel_example_tail.png" alt="camel_example_tail" /></p>

<p>代码上，即:</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">guesser</code>发起提问，<code class="language-plaintext highlighter-rouge">responder</code>回答</li>
  <li>每次提问和回答，都是一次大模型交互确定，也就是<code class="language-plaintext highlighter-rouge">guesser</code>要问什么问题，<code class="language-plaintext highlighter-rouge">responder</code>如何回答，会通过 LLM 确定。</li>
</ol>

<p>整体比较简单，我们只需要再进一步搞清楚一个问题，<code class="language-plaintext highlighter-rouge">guesser</code>是如何一步步缩小范围的。</p>

<p>看看<code class="language-plaintext highlighter-rouge">step</code>的实现：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ChatAgent</span><span class="p">(</span><span class="n">BaseAgent</span><span class="p">):</span>

    <span class="o">@</span><span class="n">observe</span><span class="p">()</span>
    <span class="k">def</span> <span class="nf">step</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">input_message</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="n">BaseMessage</span><span class="p">,</span> <span class="nb">str</span><span class="p">],</span>
        <span class="n">response_format</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">Type</span><span class="p">[</span><span class="n">BaseModel</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ChatAgentResponse</span><span class="p">:</span>
        <span class="p">...</span>
        <span class="c1"># Add user input to memory
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">update_memory</span><span class="p">(</span><span class="n">input_message</span><span class="p">,</span> <span class="n">OpenAIBackendRole</span><span class="p">.</span><span class="n">USER</span><span class="p">)</span>
</code></pre></div></div>

<p>答案就在<code class="language-plaintext highlighter-rouge">update_memory</code>里。仔细观察前面的问答图，也可以发现 token 数量越来越多。因为之前问答的上下文，都已经包含在了请求里。</p>

<h1 id="2-autogen">2. AutoGen</h1>

<h2 id="21-什么是-autogen">2.1. 什么是 AutoGen</h2>

<p>AutoGen 也是一个多智能体框架，论文也主要以介绍框架思想为主。从我初步使用看，框架确实比较好用，兼顾了易用性和灵活性。<strong>最有意思的是支持了人类的交互。这种折中的设计，其实我觉得是目前 LLM 不确定时所必须的。</strong></p>

<p>从论文的几个图来说说我的理解。</p>

<p>论文 Figure-1: <img src="/assets/images/ai-paper/autogen_figure_1.png" alt="autogen_figure_1" /></p>

<ol>
  <li>Left: conversable, customizable, and can be based on LLMs, tools, humans, or even
a combination of them，也就是 Agent 底层可以调用的能力。框架里也实现了<a href="https://docs.ag2.ai/latest/docs/api-reference/autogen/ConversableAgent/">ConversableAgent</a></li>
  <li>Top-middle: Agents can converse to solve tasks. 聊着天就把问题解决了（嗯，这年头不这么吹牛逼好像就是思想太保守了🙂‍↕️）</li>
  <li>Right: They can form a chat, potentially with humans in the loop.这点其实是当前这个框架让我觉得最特殊的地方。</li>
  <li>Bottom-middle: The framework supports flexible conversation patterns.（可能就是灵活吧，没有很理解）</li>
</ol>

<p>论文 Figure-2: <img src="/assets/images/ai-paper/autogen_figure_2.png" alt="autogen_figure_2" /></p>

<p>图里描述了一个<code class="language-plaintext highlighter-rouge">UserProxyAgent</code>和<code class="language-plaintext highlighter-rouge">AssistantAgent</code>交互，以完成代码开发的过程。其中<code class="language-plaintext highlighter-rouge">GroupChatManager</code>没有看到使用，看代码类似于一个超级大脑（人类）的感觉。</p>

<p>看到这里的时候，我就在想这个好像就是 CAMEL 要解决的问题😂。不过 AutoGen 也可以说，你可以只用<code class="language-plaintext highlighter-rouge">AssistantAgent</code>达到效果，现在 AI 论文也是混战，百花齐放百家争鸣的感觉，而且很卷，写篇论文还附带一个框架的。。。</p>

<p>论文地址：<a href="https://arxiv.org/pdf/2308.08155">AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation</a></p>

<h2 id="22-实践">2.2. 实践</h2>

<p>使用如下代码，也可以起到“猜物品”游戏的效果。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python
# coding=utf-8
</span><span class="kn">import</span> <span class="nn">os</span>

<span class="c1"># 导入autogen包
</span><span class="kn">import</span> <span class="nn">autogen</span>

<span class="kn">from</span> <span class="nn">util</span> <span class="kn">import</span> <span class="n">ARK_API_URL</span><span class="p">,</span> <span class="n">DOUBAO_SEED_1_6</span>
<span class="c1"># !/usr/bin/env python
# coding=utf-8
</span><span class="kn">import</span> <span class="nn">os</span>

<span class="c1"># 导入autogen包
</span><span class="kn">import</span> <span class="nn">autogen</span>

<span class="kn">from</span> <span class="nn">util</span> <span class="kn">import</span> <span class="n">ARK_API_URL</span><span class="p">,</span> <span class="n">DOUBAO_SEED_1_6</span>

<span class="c1">#配置大模型
</span><span class="n">llm_config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"config_list"</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="s">"model"</span><span class="p">:</span> <span class="n">DOUBAO_SEED_1_6</span><span class="p">,</span>
            <span class="s">"api_key"</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"ARK_API_KEY"</span><span class="p">),</span>
            <span class="s">"base_url"</span><span class="p">:</span> <span class="n">ARK_API_URL</span>
        <span class="p">}</span>
    <span class="p">],</span>
<span class="p">}</span>

<span class="c1"># 设置目标词
</span><span class="n">target_word</span> <span class="o">=</span> <span class="s">"袋鼠"</span>

<span class="n">responder</span> <span class="o">=</span> <span class="n">autogen</span><span class="p">.</span><span class="n">AssistantAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"回答者"</span><span class="p">,</span>
    <span class="n">llm_config</span><span class="o">=</span> <span class="n">llm_config</span><span class="p">,</span>
    <span class="n">is_termination_msg</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"content"</span><span class="p">,</span> <span class="s">""</span><span class="p">)</span> <span class="ow">and</span> <span class="n">x</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"content"</span><span class="p">,</span> <span class="s">""</span><span class="p">).</span><span class="n">rstrip</span><span class="p">().</span><span class="n">endswith</span><span class="p">(</span><span class="s">"恭喜你猜对了"</span><span class="p">),</span>
    <span class="n">system_message</span><span class="o">=</span><span class="sa">f</span><span class="s">"""
    你正在参与一个猜物品名字的游戏，注意物品名字可能细化到品牌、分类等。你的任务是根据对方提出的“是/不是”问题，以特定方式回应，帮助对方猜出目标物品。
    目标词是： </span><span class="si">{</span><span class="n">target_word</span><span class="si">}</span><span class="s">
    对方会通过问“是/不是”问题来尝试猜出这个词。你只能按照以下规则回答：
    - 如果对方的问题是“是/不是”类型，你只能回答“是”或“不是”或“有”或“没有”。
    - 如果对方说出的词就是目标词，你要回答：“恭喜你猜对了！”
    - 如果对方说出的词是目标词的父类型，你需要回答：“是这个类型，但是词不对”
    - 除此之外不能泄露任何信息。
    """</span>
<span class="p">)</span>

<span class="n">guesser</span> <span class="o">=</span> <span class="n">autogen</span><span class="p">.</span><span class="n">AssistantAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"猜词者"</span><span class="p">,</span>
    <span class="n">llm_config</span><span class="o">=</span><span class="n">llm_config</span><span class="p">,</span>
    <span class="n">system_message</span><span class="o">=</span><span class="s">"""
    你正在玩一个猜物品名字的游戏，你不知道目标词是什么，需要通过提出‘是/不是’的问题来逐步缩小范围，直到猜中。
    游戏规则如下：
    - 每次只能提出一个‘是/不是’的问题。
    - 如果你认为你知道答案，可以说“这个词是___”。
    - 如果你猜对了，对方会说“恭喜你猜对了！”。
    - 如果你说出了父类，对方会说“是这个类型，但是词不对”。
    - 请你根据之前的提问和回答合理地提出下一个问题，直到你猜中或者你说“结束”。
    """</span>
<span class="p">)</span>

<span class="c1"># 创建用户代理
# user_proxy = autogen.UserProxyAgent(
#     name="用户代理",
#     human_input_mode="ALWAYS",
#     is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("结束"),
#     code_execution_config={
#         "last_n_messages": 1,
#         "work_dir": "tasks",
#         "use_docker": False,
#     }
# )
</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="c1"># 发起对话
</span>    <span class="n">chat_results</span> <span class="o">=</span> <span class="n">autogen</span><span class="p">.</span><span class="n">initiate_chats</span><span class="p">(</span>
        <span class="p">[</span>
            <span class="p">{</span>
                <span class="s">"sender"</span><span class="p">:</span> <span class="n">guesser</span><span class="p">,</span>
                <span class="s">"recipient"</span><span class="p">:</span> <span class="n">responder</span><span class="p">,</span>
                <span class="s">"message"</span><span class="p">:</span> <span class="s">"请问这是一个生活用品吗？"</span><span class="p">,</span>
                <span class="s">"carryover"</span><span class="p">:</span> <span class="s">"你需要根据过去问的问题及答案，询问一个新的问题，这个新的问题需要有助于你最终猜到物品。"</span><span class="p">,</span>
            <span class="p">}</span>
        <span class="p">]</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>从代码也可以看到，可以很方便的切换到人类交互。</p>

<h1 id="3-总结">3. 总结</h1>

<p>通过对 CAMEL 和 AutoGen 的原理分析与实践对比，我们可以看到两种框架在设计哲学上的差异：</p>
<ul>
  <li><strong>CAMEL</strong>：追求的是<strong>完全自主</strong>的智能体社会。它通过严格的角色扮演和指令设计，试图将人类从协作流程中解放出来，更适合执行定义明确、无需中途干预的任务。</li>
  <li><strong>AutoGen</strong>：追求的是<strong>灵活的人机协作</strong>。它将人类视为系统中的一个特殊 Agent，允许随时介入，更适合探索性强、结果不确定、需要人类智慧兜底的复杂任务。</li>
</ul>

<p>在实践过程中，我也发现了一些当前多智能体框架共同面临的挑战：</p>
<ol>
  <li><strong>熵增风险</strong>：将多个尚不完全成熟的单智能体连接起来，系统的整体可靠性是提升了还是下降了？我相信会逐步有论文从理论上证明，彼时也就更能清晰的指出多智能体的发展方向</li>
  <li><strong>成本考量</strong>：多智能体协作通常意味着更多的 LLM 调用，这带来了 Token 消耗和响应时长的显著增加。论文大多只提到了效果提升，成本、时长却被忽略了。</li>
  <li><strong>上下文管理</strong>：随着对话轮次增加，携带全部历史记录会导致请求体急剧膨胀。如何有效地对上下文进行筛选或压缩，是决定系统能否扩展的关键。这里给我的感觉，就像是大数据处理时，衡量把计算放在客户端还是 HBase 的 CoProcessor  也就是服务端去做。</li>
  <li><strong>终止条件</strong>：无论是 CAMEL 还是 AutoGen，我们都必须设计一个最大迭代次数或明确的终止信号，以防止智能体陷入无限循环，这从侧面反映了当前技术方案的局限性。</li>
</ol>

<p>这也是未来在架构上值得探索的，不只是追求能返回准确结果。</p>]]></content><author><name>ying</name></author><category term="read" /><category term="ai" /><summary type="html"><![CDATA[本文是我在阅读《大模型应用开发 动手做AI Agent》时，对书中推荐的几篇核心论文的理解与总结，主要聚焦于CAMEL和 AutoGen 这两种范式，都是用来实现 MultiAgent 的。 1. CAMEL 1.1. 什么是 CAMEL? CAMEL 是 Communicative Agents for “Mind” Exploration of Large Language Model Society 的简称。 论文只比 ReAct 晚半年发表，提出了一种让智能体间互相协作的方法，中间过程无需人为干预。 1.2. CAMEL 的背景和想法 CAMEL 的研究团队，发现人们在使用 LLM 解决复杂任务时，还是非常依赖人力投入的。投入主要在于人们需要不断和智能体沟通，以确保对话朝着正确的方向发展。 那是否可以让智能体之间互相合作/监督？我感觉这里有点像是训练任务里对抗网络的意思。 CAMEL 的做法是让智能体之间对话，只需要最开始时人为设定任务、Prompt，之后就由智能体探索并给出答案。 为此，CAMEL 提出了 RolePlaying 的概念，没错，就是 RPG 游戏里的 RP😅 每个 Agent 都会遵守角色设定分工合作，不同 Agent 之间还是用自然语言通信。说白了，相比单 Agent 的变化就是： 原来 Agent 的输出发给人，人为判断后给出新的输入。 现在 Agent 的输出发给另一个 Agent 作为输入，反之亦然 思路上还是用 Agent 来替代人，如果发现不可替代，那就多搞几个 Agent ，还不行的话，或许就搞一个超级Agent(替代人做决策判断)。 扯远了，回到论文本身。论文还提出一种观点，Agent 之间可以用自然语言通信。 Communicative Agents. Communication between agents has been studied for a long time [76,77]. There are many ways to facilitate communication between agents, and with agents [29, 90,97]. Among these, natural language is considered the most natural form of communication [97]. 上图就是 CAMEL 思想的例子，左边是人为输入部分，设定两部分内容： Idea: 需要 Agents 去探讨的想法、完成的任务等 Role Assignment: 为不同的 Agent 设置不同的角色 之后就是让右边的 Agent 不断交互了。 论文地址：CAMEL: Communicative Agents for “Mind” Exploration of Large Language Model Society 此外作者探索过程中，也发现了一些让 Agent 之间合作完成任务的难点，例如角色互换、复读机一样循环等问题： Several challenges arise when asking a society of agents to autonomously cooperate on completing tasks. Examples we encountered in our preliminary analysis include role flipping, assistant repeating instructions, flake replies, and infinite loop of messages. Therefore, it is critical to investigate ways to align these models with human intentions and to explore means enabling their effective cooperation. 这也是 RolePlaying 提出来的原因，每个智能体扮演好自己的角色，不要试图跨越。 所以论文的 Prompt 里会出现： # 助手 Prompt 示例 Never forget you are a &lt;ASSISTANT_ROLE&gt; and I am a &lt;USER_ROLE&gt;. Never flip roles! # 用户 Prompt 示例 Never forget you are a &lt;USER_ROLE&gt; and I am a &lt;ASSISTANT_ROLE&gt;. Never flip roles! 1.3. 实践 按照论文的思想，手写 Prompt 也可以实现。不过作者同时开源了camel-ai/camel框架，因此我们基于这个框架验证。 为了直观感受 CAMEL 的运作机制，我设计了一个“猜物品”游戏： 回答者 (Responder)：知道目标物品（如“袋鼠”），但只能对问题回答“是/否/有/没有”等。 猜测者 (Guesser)：不知道物品，需要通过提问来猜出答案。 1.3.1 使用RolePlaying-未调通 第一次验证我使用的是RolePlaying. 但是实际效果很差，主要是两点： 由于是传入统一的task_prompt指定不同 role 的指令，因此猜物品的 Agent 其实知道是什么物品，就会按照这个方向问。比如我设定物品是袋鼠，Agent 就会问“哺乳动物”、“澳大利亚”这些词，感觉 Agent 只是为了执行你说的这个过程，而结果他早就知道了。 由于 System Prompt 是固化的，所以返回值也是固化的，比如Solution Instruction这些 具体代码：https://github.com/izualzhy/AI-Systems/blob/main/misc/camel_guess_item.py 看实现，该类通过任务描述和 Agent 的名字，自动为每个 Agent 生成其角色指令。正如论文里的观点，代码里也定义了assistant user角色，例如都有哪几种角色： # enums.py class RoleType(Enum): ASSISTANT = "assistant" USER = "user" CRITIC = "critic" EMBODIMENT = "embodiment" DEFAULT = "default" 都有哪些 System Prompt # code.py ASSISTANT_PROMPT = TextPrompt( """Never forget you are a Computer Programmer and I am a person working in {domain}. Never flip roles! Never instruct me! We share a common interest in collaborating to successfully complete a task. You must help me to complete the task using {language} programming language. ...""" # ai_society.py ASSISTANT_PROMPT: TextPrompt = TextPrompt("""===== RULES OF ASSISTANT ===== Never forget you are a {assistant_role} and I am a {user_role}. Never flip roles! Never instruct me! We share a common interest in collaborating to successfully complete a task. You must help me to complete the task. ...""" 可以在camel/camel/prompts查看相关源码。 所以整个封装是比较重的，适合实现的场景也跟这些内置的 Prompt Role 有关。 虽然不符合预期，但也不是全无收获。在RolePlaying的实现里，看到了ChatAgent这个类： class RolePlaying: self.assistant_agent: ChatAgent self.user_agent: ChatAgent 于是我用ChatAgent进行了第二次测试。 1.3.2 使用ChatAgent-符合预期 了解了原理，也可以自己实现，比如《大模型应用开发 动手做AI Agent》里的例子:https://github.com/izualzhy/AI-Systems/blob/main/HandsOnAIAgent/camel_agent_demo.py。 为了熟悉下 CAMEL 框架，我还是上一节看到的ChatAgent。 废话不多说，show code: #!/usr/bin/env python # coding=utf-8 import os from camel.agents import ChatAgent from camel.configs import ChatGPTConfig from camel.models import ModelFactory from camel.types import ModelPlatformType from colorama import Fore from util import DOUBAO_SEED_1_6, ARK_API_URL # 设置目标词 target_word = "袋鼠" doubao_config = ChatGPTConfig( temperature=0.0, top_p=None, max_tokens=None, stream=False, stop=None, ) doubao_model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI_COMPATIBLE_MODEL, model_type=DOUBAO_SEED_1_6, model_config_dict=doubao_config.as_dict(), url=ARK_API_URL, api_key=os.environ["ARK_API_KEY"] ) # 设置猜词者 system message（不能包含目标词） guesser_sys_msg = """ 你将参与一个猜物品名字的游戏。你不知道物品是什么，需要通过不断提出‘是/不是’的问题来逐步猜出这个物品。 以下是游戏的规则： - 每次只能提出一个‘是/不是’的问题。不要一次提多个问题。 - 当你认为你知道这个物品是什么时，请直接说出：‘这个词是___’。 - 只有当对方说“恭喜你猜对了！”，才说明你猜对了。 - 如果你的问题是“是/不是”类型，对方只回答“是”或“不是”或“有”或“没有”。 - 如果你说出的词是目标词的父类型，对方会回答：“是这个类型，但是词不对” """ # 设置回答者 system message（包含目标词） responder_sys_msg = f""" 你正在参与一个猜物品名字的游戏，注意物品名字可能细化到品牌、分类等。你的任务是根据对方提出的“是/不是”问题，以特定方式回应，帮助对方猜出目标物品。 目标词是： {target_word} 对方会通过问“是/不是”问题来尝试猜出这个词。你只能按照以下规则回答： - 如果对方的问题是“是/不是”类型，你只能回答“是”或“不是”或“有”或“没有”。 - 如果对方说出的词就是目标词，你要回答：“恭喜你猜对了！” - 如果对方说出的词是目标词的父类型，你需要回答：“是这个类型，但是词不对” - 除此之外不能泄露任何信息。 """ # 创建两个 Agent guesser = ChatAgent(system_message=guesser_sys_msg, model=doubao_model, output_language="zh-CN") responder = ChatAgent(system_message=responder_sys_msg, model=doubao_model, output_language="zh-CN") # 初始化对话 guesser.reset() responder.reset() print(f"开始游戏：{guesser} 和 {responder}") print(f"{guesser} 的系统信息：{guesser.role_name} {guesser.role_type} {guesser.chat_history} ") print(f"{responder} 的系统信息：{responder.role_name} {responder.role_type} {responder.chat_history} ") # 启动对话循环 responder_reply = None for step in range(100): print(Fore.BLUE + f"\n🌀 第 {step} 轮对话") # 猜词者提问 if step == 0: guesser_reply = guesser.step("开始") else: guesser_reply = guesser.step(responder_reply.msg) print(Fore.YELLOW + f"猜词者：{guesser_reply.msg.content.strip()}") print(Fore.CYAN + str(guesser_reply.info.get('usage'))) # print(f"{guesser} 的历史信息：{guesser.chat_history} ") # print(f"{guesser} 的记忆：{guesser.memory.get_context()}") # 回答者回应 responder_reply = responder.step(guesser_reply.msg) print(Fore.GREEN + f"回答者：{responder_reply.msg.content.strip()}") print(Fore.CYAN + str(responder_reply.info.get('usage'))) # print(f"{responder} 的历史信息：{responder.chat_history} ") # print(f"{responder} 的记忆：{responder.memory.get_context()}") # 判断是否猜对 if "恭喜你猜对了" in responder_reply.msg.content: print("\n🎉 游戏结束：猜词成功！") break # 更新对话输入 responder_msg = responder_reply.msg 分别创建两个ChatAgent，指定system_message， 可以看到执行效果上完全符合预期： 继续。。。 代码上，即: guesser发起提问，responder回答 每次提问和回答，都是一次大模型交互确定，也就是guesser要问什么问题，responder如何回答，会通过 LLM 确定。 整体比较简单，我们只需要再进一步搞清楚一个问题，guesser是如何一步步缩小范围的。 看看step的实现： class ChatAgent(BaseAgent): @observe() def step( self, input_message: Union[BaseMessage, str], response_format: Optional[Type[BaseModel]] = None, ) -&gt; ChatAgentResponse: ... # Add user input to memory self.update_memory(input_message, OpenAIBackendRole.USER) 答案就在update_memory里。仔细观察前面的问答图，也可以发现 token 数量越来越多。因为之前问答的上下文，都已经包含在了请求里。 2. AutoGen 2.1. 什么是 AutoGen AutoGen 也是一个多智能体框架，论文也主要以介绍框架思想为主。从我初步使用看，框架确实比较好用，兼顾了易用性和灵活性。最有意思的是支持了人类的交互。这种折中的设计，其实我觉得是目前 LLM 不确定时所必须的。 从论文的几个图来说说我的理解。 论文 Figure-1: Left: conversable, customizable, and can be based on LLMs, tools, humans, or even a combination of them，也就是 Agent 底层可以调用的能力。框架里也实现了ConversableAgent Top-middle: Agents can converse to solve tasks. 聊着天就把问题解决了（嗯，这年头不这么吹牛逼好像就是思想太保守了🙂‍↕️） Right: They can form a chat, potentially with humans in the loop.这点其实是当前这个框架让我觉得最特殊的地方。 Bottom-middle: The framework supports flexible conversation patterns.（可能就是灵活吧，没有很理解） 论文 Figure-2: 图里描述了一个UserProxyAgent和AssistantAgent交互，以完成代码开发的过程。其中GroupChatManager没有看到使用，看代码类似于一个超级大脑（人类）的感觉。 看到这里的时候，我就在想这个好像就是 CAMEL 要解决的问题😂。不过 AutoGen 也可以说，你可以只用AssistantAgent达到效果，现在 AI 论文也是混战，百花齐放百家争鸣的感觉，而且很卷，写篇论文还附带一个框架的。。。 论文地址：AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation 2.2. 实践 使用如下代码，也可以起到“猜物品”游戏的效果。 #!/usr/bin/env python # coding=utf-8 import os # 导入autogen包 import autogen from util import ARK_API_URL, DOUBAO_SEED_1_6 # !/usr/bin/env python # coding=utf-8 import os # 导入autogen包 import autogen from util import ARK_API_URL, DOUBAO_SEED_1_6 #配置大模型 llm_config = { "config_list": [ { "model": DOUBAO_SEED_1_6, "api_key": os.environ.get("ARK_API_KEY"), "base_url": ARK_API_URL } ], } # 设置目标词 target_word = "袋鼠" responder = autogen.AssistantAgent( name="回答者", llm_config= llm_config, is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("恭喜你猜对了"), system_message=f""" 你正在参与一个猜物品名字的游戏，注意物品名字可能细化到品牌、分类等。你的任务是根据对方提出的“是/不是”问题，以特定方式回应，帮助对方猜出目标物品。 目标词是： {target_word} 对方会通过问“是/不是”问题来尝试猜出这个词。你只能按照以下规则回答： - 如果对方的问题是“是/不是”类型，你只能回答“是”或“不是”或“有”或“没有”。 - 如果对方说出的词就是目标词，你要回答：“恭喜你猜对了！” - 如果对方说出的词是目标词的父类型，你需要回答：“是这个类型，但是词不对” - 除此之外不能泄露任何信息。 """ ) guesser = autogen.AssistantAgent( name="猜词者", llm_config=llm_config, system_message=""" 你正在玩一个猜物品名字的游戏，你不知道目标词是什么，需要通过提出‘是/不是’的问题来逐步缩小范围，直到猜中。 游戏规则如下： - 每次只能提出一个‘是/不是’的问题。 - 如果你认为你知道答案，可以说“这个词是___”。 - 如果你猜对了，对方会说“恭喜你猜对了！”。 - 如果你说出了父类，对方会说“是这个类型，但是词不对”。 - 请你根据之前的提问和回答合理地提出下一个问题，直到你猜中或者你说“结束”。 """ ) # 创建用户代理 # user_proxy = autogen.UserProxyAgent( # name="用户代理", # human_input_mode="ALWAYS", # is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("结束"), # code_execution_config={ # "last_n_messages": 1, # "work_dir": "tasks", # "use_docker": False, # } # ) if __name__ == '__main__': # 发起对话 chat_results = autogen.initiate_chats( [ { "sender": guesser, "recipient": responder, "message": "请问这是一个生活用品吗？", "carryover": "你需要根据过去问的问题及答案，询问一个新的问题，这个新的问题需要有助于你最终猜到物品。", } ] ) 从代码也可以看到，可以很方便的切换到人类交互。 3. 总结 通过对 CAMEL 和 AutoGen 的原理分析与实践对比，我们可以看到两种框架在设计哲学上的差异： CAMEL：追求的是完全自主的智能体社会。它通过严格的角色扮演和指令设计，试图将人类从协作流程中解放出来，更适合执行定义明确、无需中途干预的任务。 AutoGen：追求的是灵活的人机协作。它将人类视为系统中的一个特殊 Agent，允许随时介入，更适合探索性强、结果不确定、需要人类智慧兜底的复杂任务。 在实践过程中，我也发现了一些当前多智能体框架共同面临的挑战： 熵增风险：将多个尚不完全成熟的单智能体连接起来，系统的整体可靠性是提升了还是下降了？我相信会逐步有论文从理论上证明，彼时也就更能清晰的指出多智能体的发展方向 成本考量：多智能体协作通常意味着更多的 LLM 调用，这带来了 Token 消耗和响应时长的显著增加。论文大多只提到了效果提升，成本、时长却被忽略了。 上下文管理：随着对话轮次增加，携带全部历史记录会导致请求体急剧膨胀。如何有效地对上下文进行筛选或压缩，是决定系统能否扩展的关键。这里给我的感觉，就像是大数据处理时，衡量把计算放在客户端还是 HBase 的 CoProcessor 也就是服务端去做。 终止条件：无论是 CAMEL 还是 AutoGen，我们都必须设计一个最大迭代次数或明确的终止信号，以防止智能体陷入无限循环，这从侧面反映了当前技术方案的局限性。 这也是未来在架构上值得探索的，不只是追求能返回准确结果。]]></summary></entry><entry><title type="html">Read &amp;amp; Practice-读《大模型应用开发 动手做AI Agent》</title><link href="https://izualzhy.cn/hands-on-ai-agent-reading" rel="alternate" type="text/html" title="Read &amp;amp; Practice-读《大模型应用开发 动手做AI Agent》" /><published>2025-07-05T12:18:18+00:00</published><updated>2025-07-05T12:18:18+00:00</updated><id>https://izualzhy.cn/hands-on-ai-agent-reading</id><content type="html" xml:base="https://izualzhy.cn/hands-on-ai-agent-reading"><![CDATA[<p><img src="https://izualzhy.cn/assets/images/book/s34857089.jpg" alt="大模型应用开发 动手做AI Agent" /></p>

<p>AI很有意思，但是无论是 AI 本身需要的数据，还是关于 AI 的文章，鱼龙混杂。</p>

<p>这本书我觉得值得一读，而且“纸上觉来终觉浅，绝知此事要躬行”。Agent这个东西，描绘了一个足够美好的场景，同时技术现状总是偶尔给人一些惊喜，让人眼前一亮，所以都在忍不住尝试。 然而实际情况却是好用的agent凤毛麟角，倒是一堆人争着去做Agent框架和平台、讨论Agent定义。</p>

<p>同时Agent的不确定性，也导致只能本地改改代码、出出主意，让规划行程、订酒店机票的真是扯淡。明知现状做不到，可是却又都期待着，这就是相信的力量吧？</p>

<p>回到书籍本身，一边旁征博引经典论文，一边举着浅显又效果很差的例子，也是太难为作者了😓。好在作者功力很强，书籍读起来不乏味，也收获很多思考。</p>

<p>书里推荐了一些论文，我觉得也是非常值得一读的:</p>
<ul>
  <li><a href="https://izualzhy.cn/llm-paper-read-cot-react">LLM Paper&amp;Practice：从 CoT 到 ReAct</a></li>
  <li><a href="https://izualzhy.cn/llm-paper-read-camel-autogen">LLM Paper&amp;Practice: CAMEL 和 AutoGen</a></li>
</ul>]]></content><author><name>ying</name></author><category term="read" /><summary type="html"><![CDATA[AI很有意思，但是无论是 AI 本身需要的数据，还是关于 AI 的文章，鱼龙混杂。 这本书我觉得值得一读，而且“纸上觉来终觉浅，绝知此事要躬行”。Agent这个东西，描绘了一个足够美好的场景，同时技术现状总是偶尔给人一些惊喜，让人眼前一亮，所以都在忍不住尝试。 然而实际情况却是好用的agent凤毛麟角，倒是一堆人争着去做Agent框架和平台、讨论Agent定义。 同时Agent的不确定性，也导致只能本地改改代码、出出主意，让规划行程、订酒店机票的真是扯淡。明知现状做不到，可是却又都期待着，这就是相信的力量吧？ 回到书籍本身，一边旁征博引经典论文，一边举着浅显又效果很差的例子，也是太难为作者了😓。好在作者功力很强，书籍读起来不乏味，也收获很多思考。 书里推荐了一些论文，我觉得也是非常值得一读的: LLM Paper&amp;Practice：从 CoT 到 ReAct LLM Paper&amp;Practice: CAMEL 和 AutoGen]]></summary></entry><entry><title type="html">LLM Paper&amp;amp;Practice：从 CoT 到 ReAct</title><link href="https://izualzhy.cn/llm-paper-read-cot-react" rel="alternate" type="text/html" title="LLM Paper&amp;amp;Practice：从 CoT 到 ReAct" /><published>2025-07-05T11:42:17+00:00</published><updated>2025-07-05T11:42:17+00:00</updated><id>https://izualzhy.cn/llm-paper-read-cot-react</id><content type="html" xml:base="https://izualzhy.cn/llm-paper-read-cot-react"><![CDATA[<p>本文是我在阅读《大模型应用开发 动手做AI Agent》时，对书中推荐的几篇核心论文的理解与总结，主要聚焦于<strong>思维链（Chain-of-Thought, CoT）</strong>和 <strong>ReAct</strong> 这两种范式。</p>

<p>学术论文往往较为抽象，本文尽量做到理论与实践相结合，通过阅读论文和代码实践，来验证和剖析这些技术背后的思想，并分享我在此过程中的一些思考与困惑。</p>

<h1 id="1-思维链chain-of-thought-cot">1. 思维链（Chain-of-Thought, CoT）</h1>

<p>教会模型“一步一步想”。</p>

<h2 id="11-什么是-cot">1.1. 什么是 CoT？</h2>

<p>思维链（Chain-of-Thought）是一种提示（Prompting）技术，它通过在提示中加入一些“逐步思考”的范例（Few-shot），来引导 LLM 在回答问题时，模仿范例，输出一个详细的、循序渐进的推理过程，并最终给出答案。</p>

<p>这个过程就像我们人类解决复杂问题时，会把问题分解成若干个小步骤，然后逐一解决。CoT 的核心思想就是将这种“思考过程”显式地展示出来。</p>

<p>详细论文参考：<a href="https://arxiv.org/abs/2201.11903">Chain-of-Thought Prompting Elicits Reasoning in Large Language Models</a></p>

<h2 id="12-为什么需要-cot">1.2. 为什么需要 CoT？</h2>

<p>标准的 LLM 在处理复杂问题，尤其是数学应用题、逻辑推理题时，很容易因为“一步到位”的思考模式而得出错误结论。它们往往只关注表面的关联，而缺乏深度的逻辑演绎。</p>

<p>CoT 的出现，恰好弥补了这一点。它强迫模型放慢脚步，关注过程而非仅仅是结果，从而显著提升了在推理任务上的准确性。</p>

<p>注意论文是在 2022年1月 发表的，之所以笔记标题叫做炒冷饭，原因也在于此。虽然只隔了三年时间，但是大模型的发展迅速，有些例子已经不适用。之前说“一日不见如隔三秋”，放在 AI 领域真是太合适不过，三年已过，如同上个世纪。</p>

<h2 id="13-实践">1.3. 实践</h2>

<p>论文中这个被广泛引用的图例，直观地展示了 CoT 的作用：</p>

<p><img src="/assets/images/ai-paper/cot_figure_1.png" alt="cot_figure_1" /></p>

<p>我使用代码进行了实际测试：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">testCOT</span><span class="p">(</span><span class="n">llm</span><span class="p">:</span> <span class="n">ChatOpenAI</span><span class="p">):</span>
    <span class="c1"># Standard Prompting
</span>    <span class="n">question_standard</span> <span class="o">=</span> <span class="s">"""Q: Roger has 5 tennis balls. He buys 2 more cans of
tennis balls. Each can has 3 tennis balls. How many
tennis balls does he have now?
A: The answer is 11.
Q: The cafeteria had 23 apples. If they used 20 to
make lunch and bought 6 more, how many apples
do they have?"""</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'-'</span><span class="o">*</span><span class="mi">32</span> <span class="o">+</span> <span class="s">' Standard Prompting '</span> <span class="o">+</span> <span class="s">'-'</span><span class="o">*</span><span class="mi">32</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">invokeLLM</span><span class="p">(</span><span class="n">llm</span><span class="p">,</span> <span class="n">question_standard</span><span class="p">))</span>

    <span class="c1"># Chain-of-Thought Prompting
</span>    <span class="n">question_cot</span> <span class="o">=</span> <span class="s">""""Q: Roger has 5 tennis balls. He buys 2 more cans of
tennis balls. Each can has 3 tennis balls. How many
tennis balls does he have now?
A: Roger started with 5 balls. 2 cans of 3 tennis balls
each is 6 tennis balls. 5 + 6 = 11. The answer is 11.
Q: The cafeteria had 23 apples. If they used 20 to
make lunch and bought 6 more, how many apples
do they have?"""</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'-'</span><span class="o">*</span><span class="mi">32</span> <span class="o">+</span> <span class="s">' CoT Prompting '</span> <span class="o">+</span> <span class="s">'-'</span><span class="o">*</span><span class="mi">32</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">invokeLLM</span><span class="p">(</span><span class="n">llm</span><span class="p">,</span> <span class="n">question_cot</span><span class="p">))</span>
</code></pre></div></div>

<p>这是用 doubaoseed1.6 某次输出的效果：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-------------------------------- Standard Prompting --------------------------------
The cafeteria starts with 23 apples. They use 20, so subtract 20: 23 - 20 = 3. Then they buy 6 more, so add 6: 3 + 6 = 9. The answer is 9.
-------------------------------- CoT Prompting --------------------------------
A: The cafeteria started with 23 apples. They used 20 apples, so 23 - 20 = 3 apples left. Then they bought 6 more, so 3 + 6 = 9. The answer is 9.
</code></pre></div></div>

<p>doubao1.5visionlite 某次输出的效果：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-------------------------------- Standard Prompting --------------------------------
First, find out how many apples are left after using some:
The cafeteria had 23 apples and used 20, so the number of apples left is \(23 - 20= 3\).
Then, they bought 6 more apples. So the total number of apples now is \(3 + 6 = 9\).
The answer is 9.
-------------------------------- CoT Prompting --------------------------------
The cafeteria started with 23 apples. They used 20 apples, so they had 23 - 20 = 3 apples left. Then they bought 6 more apples. 3 + 6 = 9. The answer is 9.
</code></pre></div></div>

<p>用 deepseek-r1 某次输出的效果：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-------------------------------- Standard Prompting --------------------------------

The cafeteria started with 23 apples.  
They used 20 apples to make lunch, which means they have 23 - 20 = 3 apples left.  
Then, they bought 6 more apples, so they have 3 + 6 = 9 apples now.  

Alternatively, the net change is -20 (used) + 6 (bought) = -14, so 23 + (-14) = 9.  
Regardless of the order of operations, the result is consistent: 23 - 20 + 6 = 9, or 23 + 6 - 20 = 9.  

Thus, the cafeteria now has 9 apples.

\boxed{9}
-------------------------------- CoT Prompting --------------------------------

The cafeteria started with 23 apples.  
They used 20 apples for lunch, so they had 23 - 20 = 3 apples left.  
Then they bought 6 more apples, so 3 + 6 = 9.  
The answer is 9.
</code></pre></div></div>

<p class="warning">可以看到用现在的大模型，回答其实都是准确的。</p>

<p>我尝试多次调用，以及输入其他问题，例如：</p>
<ol>
  <li>Mary’s father has five daughters: Nana, Nene, Nini, Nono. What is the name of the fifth daughter?(为了引导模型逐步推理，加入这一句”Let’s think step by step.”)</li>
  <li>Mike plays ping pong for 40 minutes. In the first 20 minutes, he scores 4 points. In the second 20 minutes, he scores 25% more points. How many total points did he score?</li>
</ol>

<p>即使使用 Standard Prompting 而不是 Chain-of-Thought Prompting ，模型在输出时不仅展现了推理能力，而且也能够输出正确答案。</p>

<p>每次调用结果不同，如果在 Prompt 里尝试引导模型推理，符合预期的概率确实更大一些。</p>

<p class="success">
    我的理解是大模型的效果就像是一个 V 字型，左边是训练生成模型，右边是查询模型，两边都会影响推理效果：<br />
1. <strong>模型自身已进化</strong>: 现在的很多大模型，在左边就已经使用更精确的数据、调优解决了我测试的这些 query，因此即使不使用 COT ，效果也非常好了。<br />
2. <strong>CoT 提升稳定性</strong>: 为了更大概率的拿到想要的输出，还是应当尽量使用 COT<br />
3. <strong>CoT 的历史定位</strong>: 相比后续出现的策略，COT 最为基础和直接。因为 COT 只使用了 Prompt 来提升效果，因此也是 Prompt Engine 的一种体现。(注：我查了下似乎提出 PE 只比 COT 早了半年)<br />
</p>

<p>COT 的本质是要引导大模型的推理过程，现在看其实已经是比较普遍的思想了。</p>

<p>现在想想，这也非常符合直觉。就像我们自己计算出错后，会下意识地告诉自己“再仔细想想”。“多想想，按部就班地推演一遍，就会少犯错”——这个简单的道理，在大模型领域被赋予了一个专门的术语：CoT。</p>

<p><em>相关代码放在 <a href="https://github.com/izualzhy/AI-Systems/blob/main/misc/read_paper_cot.py">https://github.com/izualzhy/AI-Systems/blob/main/misc/read_paper_cot.py</a></em></p>

<h1 id="2-react">2. ReAct</h1>

<p>从“思考”到“行动”的进化。</p>

<h2 id="21-什么是-react">2.1. 什么是 ReAct？</h2>

<p><strong>ReAct</strong> 范式可以看作是 CoT 的一个强大演进。它不仅包含了 CoT 的“推理”（Reasoning）能力，更引入了“行动”（Acting）的概念。</p>

<p>ReAct 的核心机制是一个 <code class="language-plaintext highlighter-rouge">Thought -&gt; Action -&gt; Observation</code> 的循环：</p>

<ol>
  <li><strong>Thought (思考):</strong> 模型根据当前任务和已有信息，分析现状，并规划出下一步需要做什么。</li>
  <li><strong>Action (行动):</strong> 模型决定执行一个具体的“行动”。这个行动通常是调用外部工具（API），例如进行网络搜索、查询数据库、执行代码等。</li>
  <li><strong>Observation (观察):</strong> 模型获取“行动”返回的结果。这个结果会作为新的信息，融入到下一轮的“思考”中。</li>
</ol>

<p>通过这个循环，LLM 能够与外部世界进行交互，获取实时、准确的信息，从而克服其自身知识库静态、可能过时的缺点。</p>

<p>详细论文参考：<a href="https://arxiv.org/abs/2210.03629">REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS</a></p>

<h2 id="22-为什么需要-react">2.2. 为什么需要 ReAct？</h2>

<p>CoT 极大地增强了模型的推理能力，但它的推理过程是“封闭”的，完全依赖于模型内部的知识。如果遇到以下场景，CoT 就会力不从心：</p>

<ul>
  <li><strong>知识过时：</strong> LLM 的知识截止于其训练日期。</li>
  <li><strong>需要精确计算：</strong> LLM 在数学计算上并不可靠。</li>
  <li><strong>需要与外部服务交互：</strong> 例如订票、发邮件等。</li>
</ul>

<p>ReAct 通过引入“行动”，将 LLM 从一个“封闭大脑”变成了一个能够主动探索和获取信息的“智能代理”（Agent），极大地拓展了其应用边界。</p>

<p><img src="/assets/images/ai-paper/react_figure_1.png" alt="react_figure_1" /></p>

<p>这套“思考-行动-观察”的循环，正是 ReAct Agent 的基本工作模式。</p>

<h2 id="23-实践">2.3. 实践</h2>

<p>那么，这篇学术论文的思想在工程实践中是如何应用的呢？主要体现在两个方面：</p>

<ol>
  <li><strong>精心设计的 Prompt</strong>：构建一个能引导模型输出“Thought, Action, Action Input”等结构化文本的提示。</li>
  <li><strong>外部控制循环</strong>：在代码中实现一个循环，用于解析模型输出、调用相应工具、并将工具结果返回给模型，直到任务完成。</li>
</ol>

<p>我们用 LangChain 框架来直观感受一下：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_current_weather</span><span class="p">(</span><span class="n">location</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">** [函数被调用] location = </span><span class="si">{</span><span class="n">location</span><span class="si">}</span><span class="s"> **</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">location</span><span class="si">}</span><span class="s"> 今天天气晴，25°C."</span>

<span class="n">tools</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">Tool</span><span class="p">.</span><span class="n">from_function</span><span class="p">(</span>
        <span class="n">func</span><span class="o">=</span><span class="n">get_current_weather</span><span class="p">,</span>
        <span class="n">name</span><span class="o">=</span><span class="s">"get_current_weather"</span><span class="p">,</span>
        <span class="n">description</span><span class="o">=</span><span class="s">"获取城市当前天气"</span><span class="p">,</span>
        <span class="n">handle_parsing_errors</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>
<span class="p">]</span>

<span class="c1"># 初始化 Agent
# llm = getDoubaoSeed16()
</span><span class="n">llm</span> <span class="o">=</span> <span class="n">getDoubao15VisionLite</span><span class="p">()</span>
<span class="c1"># llm = getDeepSeekR1()
</span>
<span class="k">def</span> <span class="nf">v1</span><span class="p">():</span>
    <span class="c1"># llm = getDeepSeekR1()
</span>    <span class="n">agent</span> <span class="o">=</span> <span class="n">initialize_agent</span><span class="p">(</span><span class="n">tools</span><span class="p">,</span>
                             <span class="n">llm</span><span class="p">,</span>
                             <span class="n">agent_type</span><span class="o">=</span><span class="n">AgentType</span><span class="p">.</span><span class="n">ZERO_SHOT_REACT_DESCRIPTION</span><span class="p">,</span>
                             <span class="c1"># agent_type="openai-tools",
</span>                             <span class="n">verbose</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
                             <span class="n">temperature</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
                             <span class="n">handle_parsing_errors</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">run</span><span class="p">():</span>
        <span class="k">print</span><span class="p">(</span><span class="n">agent</span><span class="p">.</span><span class="n">agent</span><span class="p">.</span><span class="n">llm_chain</span><span class="p">.</span><span class="n">prompt</span><span class="p">.</span><span class="n">template</span><span class="p">)</span>
        <span class="c1"># 用户提问
</span>        <span class="n">result</span> <span class="o">=</span> <span class="n">agent</span><span class="p">.</span><span class="n">invoke</span><span class="p">(</span><span class="s">"请问上海的天气怎么样？"</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
    
    <span class="n">run</span><span class="p">()</span>
</code></pre></div></div>

<p>上述代码输出：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Answer the following questions as best you can. You have access to the following tools:

get_current_weather(location: str) - 获取城市当前天气

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_current_weather]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}


&gt; Entering new AgentExecutor chain...
Thought: 需要获取上海当前的天气信息，调用 get_current_weather 工具来实现
Action: get_current_weather
Action Input: 上海
** [函数被调用] location = 上海 **


Observation: 上海 今天天气晴，25°C.
Thought:I now know the final answer
Final Answer: 上海 今天天气晴，25°C.

&gt; Finished chain.
</code></pre></div></div>

<p>可以看到函数被调用，且返回了预期的最终答案。</p>

<h3 id="231-prompt">2.3.1. Prompt</h3>

<p><code class="language-plaintext highlighter-rouge">ZERO_SHOT_REACT_DESCRIPTION</code> 这个 Agent 内置的 Prompt 模板，完美诠释了 ReAct 的思想：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Answer the following questions as best you can. You have access to the following tools:

get_current_weather(location: str) - 获取城市当前天气

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_current_weather]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}
</code></pre></div></div>
<p>这个模板通过清晰的指令和格式要求，引导模型遵循 <code class="language-plaintext highlighter-rouge">Thought -&gt; Action -&gt; Observation</code> 的路径进行思考和输出。</p>

<h3 id="232-循环">2.3.2. 循环</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AgentExecutor</span><span class="p">(</span><span class="n">Chain</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">_call</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">inputs</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">],</span>
        <span class="n">run_manager</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">CallbackManagerForChainRun</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
        <span class="s">"""Run text through and get agent response."""</span>
        <span class="c1"># Construct a mapping of tool name to tool for easy lookup
</span>        <span class="n">name_to_tool_map</span> <span class="o">=</span> <span class="p">{</span><span class="n">tool</span><span class="p">.</span><span class="n">name</span><span class="p">:</span> <span class="n">tool</span> <span class="k">for</span> <span class="n">tool</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">tools</span><span class="p">}</span>
        <span class="c1"># We construct a mapping from each tool to a color, used for logging.
</span>        <span class="n">color_mapping</span> <span class="o">=</span> <span class="n">get_color_mapping</span><span class="p">(</span>
            <span class="p">[</span><span class="n">tool</span><span class="p">.</span><span class="n">name</span> <span class="k">for</span> <span class="n">tool</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">tools</span><span class="p">],</span> <span class="n">excluded_colors</span><span class="o">=</span><span class="p">[</span><span class="s">"green"</span><span class="p">,</span> <span class="s">"red"</span><span class="p">]</span>
        <span class="p">)</span>
        <span class="n">intermediate_steps</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="n">AgentAction</span><span class="p">,</span> <span class="nb">str</span><span class="p">]]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="c1"># Let's start tracking the number of iterations and time elapsed
</span>        <span class="n">iterations</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">time_elapsed</span> <span class="o">=</span> <span class="mf">0.0</span>
        <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
        <span class="c1"># We now enter the agent loop (until it returns something).
</span>        <span class="k">while</span> <span class="bp">self</span><span class="p">.</span><span class="n">_should_continue</span><span class="p">(</span><span class="n">iterations</span><span class="p">,</span> <span class="n">time_elapsed</span><span class="p">):</span>
            <span class="n">next_step_output</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_take_next_step</span><span class="p">(</span>
                <span class="n">name_to_tool_map</span><span class="p">,</span>
                <span class="n">color_mapping</span><span class="p">,</span>
                <span class="n">inputs</span><span class="p">,</span>
                <span class="n">intermediate_steps</span><span class="p">,</span>
                <span class="n">run_manager</span><span class="o">=</span><span class="n">run_manager</span><span class="p">,</span>
            <span class="p">)</span>
            <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">next_step_output</span><span class="p">,</span> <span class="n">AgentFinish</span><span class="p">):</span>
                <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">_return</span><span class="p">(</span>
                    <span class="n">next_step_output</span><span class="p">,</span> <span class="n">intermediate_steps</span><span class="p">,</span> <span class="n">run_manager</span><span class="o">=</span><span class="n">run_manager</span>
                <span class="p">)</span>

            <span class="n">intermediate_steps</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">next_step_output</span><span class="p">)</span>
            <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">next_step_output</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
                <span class="n">next_step_action</span> <span class="o">=</span> <span class="n">next_step_output</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
                <span class="c1"># See if tool should return directly
</span>                <span class="n">tool_return</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_get_tool_return</span><span class="p">(</span><span class="n">next_step_action</span><span class="p">)</span>
                <span class="k">if</span> <span class="n">tool_return</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
                    <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">_return</span><span class="p">(</span>
                        <span class="n">tool_return</span><span class="p">,</span> <span class="n">intermediate_steps</span><span class="p">,</span> <span class="n">run_manager</span><span class="o">=</span><span class="n">run_manager</span>
                    <span class="p">)</span>
            <span class="n">iterations</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="n">time_elapsed</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span>
        <span class="n">output</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_action_agent</span><span class="p">.</span><span class="n">return_stopped_response</span><span class="p">(</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">early_stopping_method</span><span class="p">,</span> <span class="n">intermediate_steps</span><span class="p">,</span> <span class="o">**</span><span class="n">inputs</span>
        <span class="p">)</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">_return</span><span class="p">(</span><span class="n">output</span><span class="p">,</span> <span class="n">intermediate_steps</span><span class="p">,</span> <span class="n">run_manager</span><span class="o">=</span><span class="n">run_manager</span><span class="p">)</span>
</code></pre></div></div>

<p>循环的过程在上述代码里，遵循<code class="language-plaintext highlighter-rouge">调用大模型 -&gt; 解析大模型返回结果(action/final answer) -&gt; 执行 -&gt; 根据执行结果继续调用...</code>.</p>

<p><code class="language-plaintext highlighter-rouge">_should_continue</code>里传入了迭代次数、消耗时间，避免陷入循环一直不返回。</p>

<p>解析返回结果：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MRKLOutputParser</span><span class="p">(</span><span class="n">AgentOutputParser</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">parse</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="n">AgentAction</span><span class="p">,</span> <span class="n">AgentFinish</span><span class="p">]:</span>
        <span class="s">"""Parse the output from the agent into
        an AgentAction or AgentFinish object.

        Args:
            text: The text to parse.

        Returns:
            An AgentAction or AgentFinish object.

        Raises:
            OutputParserException: If the output could not be parsed.
        """</span>
        <span class="n">includes_answer</span> <span class="o">=</span> <span class="n">FINAL_ANSWER_ACTION</span> <span class="ow">in</span> <span class="n">text</span>
        <span class="n">regex</span> <span class="o">=</span> <span class="p">(</span>
            <span class="sa">r</span><span class="s">"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"</span>
        <span class="p">)</span>
        <span class="n">action_match</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">regex</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="n">re</span><span class="p">.</span><span class="n">DOTALL</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">action_match</span> <span class="ow">and</span> <span class="n">includes_answer</span><span class="p">:</span>
            <span class="p">...</span>
</code></pre></div></div>

<p>执行工具的调用栈：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AgentExecutor._take_next_step -&gt; 
AgentExecutor._iter_next_step -&gt;
AgentExecutor._perform_agent_action -&gt;
BaseTool.run -&gt;
Tool._run -&gt;
自定义函数
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td>由于大模型的框架都在快速迭代(换句话说不成熟😄)，就不深入展开代码了。理解概念吧。-_-</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h2 id="24-实践中的困惑">2.4. 实践中的困惑</h2>

<p>理论很丰满，但现实很骨感。实际上，我第一次运行上述代码时，花费了数小时也未能成功。</p>

<p>我发现，代码能否跑通，<strong>极度依赖所选择的 LLM</strong>。当我使用 <code class="language-plaintext highlighter-rouge">Doubao-Seed-1.6</code> 或 <code class="language-plaintext highlighter-rouge">DeepSeek-R1</code> 时，遇到了各种问题：</p>
<ol>
  <li>模型返回结果格式错乱，比如同时输出了 <code class="language-plaintext highlighter-rouge">Final Answer</code> 和 <code class="language-plaintext highlighter-rouge">Action</code>，导致框架无法判断是否应结束循环。</li>
  <li>模型“自作主张”，不调用我定义的 <code class="language-plaintext highlighter-rouge">get_current_weather</code> 工具，反而直接输出一段它自己“知道”的关于上海天气的陈旧信息。</li>
</ol>

<p>直到我换成了 <code class="language-plaintext highlighter-rouge">Doubao-1.5-vision-lite</code> 模型，一切才顺利运行。这引人深思：难道模型越高级，反而越不听话，越不遵守 Prompt 的指令了？</p>

<p class="success">
    这让我产生了更深层次的理解：<br />
1. <strong>ReAct 的本质</strong>：大模型本质是概率性的，无法保证100%准确。而现实世界需要确定性。ReAct 的出现，就是为了解决这个��盾：通过引入“工具”这个确定性组件，来弥补大模型的短板。而 Prompt 就是我们让模型学会使用工具的“说明书”。<br />
2. <strong>脆弱的控制链</strong>：目前的 Agent 实现对 `模型 + Prompt` 的组合高度敏感。更换一个模型，或者稍微修改一下 Prompt，都可能导致整个控制链条的崩溃。这暴露了当前 Agent 技术的脆弱性。<br />
3. <strong>农场里的科学家</strong>：我们现在通过精心设计的 Prompt 来控制大模型的行为，其原理更像是通过大量实验观察到的经验规律，而非精确的理论指导。这让我感觉，我们就像一群“农场鸡舍里的科学家”，试图理解并引导一群我们并未完全掌握其内在机理的“鸡”。<br />
4. <strong>工具的演进</strong>：ReAct 引入工具，最初可能只是为了获取更准确、更实时的信息。但这个思想一旦被打开，我们的想象力便不再局限于此，而是希望用 Agent 去完成更复杂、更主动的任务，这催生了当前百花齐放的 Agent ��术浪潮。
</p>
<p><em>注：相关代码在 <a href="https://github.com/izualzhy/AI-Systems/blob/main/misc/read_paper_react.py">https://github.com/izualzhy/AI-Systems/blob/main/misc/read_paper_react.py</a></em></p>

<h1 id="3-总结从思想到行动的演进">3. 总结：从思想到行动的演进</h1>

<p>回顾这两篇里程碑式的论文，我们可以看到一条清晰的技术演进路径：</p>

<ol>
  <li><strong>Standard Prompting</strong>：直接问答，完全依赖模型自身知识。</li>
  <li><strong>Chain-of-Thought (CoT)</strong>：通过引导模型思考中间步骤，提升复杂推理的可靠性。这是<strong>内部思维</strong>的优化。</li>
  <li><strong>ReAct</strong>：将 CoT 的“思考”与“行动”（使用工具）相结合，让模型能与外部世界交互，从“思考者”进化为“行动者”。这是<strong>思维与实践</strong>的结合。</li>
</ol>

<p>这个过程，是从一个封闭的“知识库”到一个能够进行“研究”和“实践”的初级 Agent 的演变。我在实践中遇到的问题，或许也是当前 Agent 开发领域正在努力解决的核心问题：
<strong>如何在给予模型强大推理能力的同时，确保它能可靠、可控地遵循我们设计的流程和规则，在需要时放下“身段”，谦虚地使用工具</strong></p>]]></content><author><name>ying</name></author><category term="read" /><category term="ai" /><summary type="html"><![CDATA[本文是我在阅读《大模型应用开发 动手做AI Agent》时，对书中推荐的几篇核心论文的理解与总结，主要聚焦于思维链（Chain-of-Thought, CoT）和 ReAct 这两种范式。 学术论文往往较为抽象，本文尽量做到理论与实践相结合，通过阅读论文和代码实践，来验证和剖析这些技术背后的思想，并分享我在此过程中的一些思考与困惑。 1. 思维链（Chain-of-Thought, CoT） 教会模型“一步一步想”。 1.1. 什么是 CoT？ 思维链（Chain-of-Thought）是一种提示（Prompting）技术，它通过在提示中加入一些“逐步思考”的范例（Few-shot），来引导 LLM 在回答问题时，模仿范例，输出一个详细的、循序渐进的推理过程，并最终给出答案。 这个过程就像我们人类解决复杂问题时，会把问题分解成若干个小步骤，然后逐一解决。CoT 的核心思想就是将这种“思考过程”显式地展示出来。 详细论文参考：Chain-of-Thought Prompting Elicits Reasoning in Large Language Models 1.2. 为什么需要 CoT？ 标准的 LLM 在处理复杂问题，尤其是数学应用题、逻辑推理题时，很容易因为“一步到位”的思考模式而得出错误结论。它们往往只关注表面的关联，而缺乏深度的逻辑演绎。 CoT 的出现，恰好弥补了这一点。它强迫模型放慢脚步，关注过程而非仅仅是结果，从而显著提升了在推理任务上的准确性。 注意论文是在 2022年1月 发表的，之所以笔记标题叫做炒冷饭，原因也在于此。虽然只隔了三年时间，但是大模型的发展迅速，有些例子已经不适用。之前说“一日不见如隔三秋”，放在 AI 领域真是太合适不过，三年已过，如同上个世纪。 1.3. 实践 论文中这个被广泛引用的图例，直观地展示了 CoT 的作用： 我使用代码进行了实际测试： def testCOT(llm: ChatOpenAI): # Standard Prompting question_standard = """Q: Roger has 5 tennis balls. He buys 2 more cans of tennis balls. Each can has 3 tennis balls. How many tennis balls does he have now? A: The answer is 11. Q: The cafeteria had 23 apples. If they used 20 to make lunch and bought 6 more, how many apples do they have?""" print('-'*32 + ' Standard Prompting ' + '-'*32) print(invokeLLM(llm, question_standard)) # Chain-of-Thought Prompting question_cot = """"Q: Roger has 5 tennis balls. He buys 2 more cans of tennis balls. Each can has 3 tennis balls. How many tennis balls does he have now? A: Roger started with 5 balls. 2 cans of 3 tennis balls each is 6 tennis balls. 5 + 6 = 11. The answer is 11. Q: The cafeteria had 23 apples. If they used 20 to make lunch and bought 6 more, how many apples do they have?""" print('-'*32 + ' CoT Prompting ' + '-'*32) print(invokeLLM(llm, question_cot)) 这是用 doubaoseed1.6 某次输出的效果： -------------------------------- Standard Prompting -------------------------------- The cafeteria starts with 23 apples. They use 20, so subtract 20: 23 - 20 = 3. Then they buy 6 more, so add 6: 3 + 6 = 9. The answer is 9. -------------------------------- CoT Prompting -------------------------------- A: The cafeteria started with 23 apples. They used 20 apples, so 23 - 20 = 3 apples left. Then they bought 6 more, so 3 + 6 = 9. The answer is 9. doubao1.5visionlite 某次输出的效果： -------------------------------- Standard Prompting -------------------------------- First, find out how many apples are left after using some: The cafeteria had 23 apples and used 20, so the number of apples left is \(23 - 20= 3\). Then, they bought 6 more apples. So the total number of apples now is \(3 + 6 = 9\). The answer is 9. -------------------------------- CoT Prompting -------------------------------- The cafeteria started with 23 apples. They used 20 apples, so they had 23 - 20 = 3 apples left. Then they bought 6 more apples. 3 + 6 = 9. The answer is 9. 用 deepseek-r1 某次输出的效果： -------------------------------- Standard Prompting -------------------------------- The cafeteria started with 23 apples. They used 20 apples to make lunch, which means they have 23 - 20 = 3 apples left. Then, they bought 6 more apples, so they have 3 + 6 = 9 apples now. Alternatively, the net change is -20 (used) + 6 (bought) = -14, so 23 + (-14) = 9. Regardless of the order of operations, the result is consistent: 23 - 20 + 6 = 9, or 23 + 6 - 20 = 9. Thus, the cafeteria now has 9 apples. \boxed{9} -------------------------------- CoT Prompting -------------------------------- The cafeteria started with 23 apples. They used 20 apples for lunch, so they had 23 - 20 = 3 apples left. Then they bought 6 more apples, so 3 + 6 = 9. The answer is 9. 可以看到用现在的大模型，回答其实都是准确的。 我尝试多次调用，以及输入其他问题，例如： Mary’s father has five daughters: Nana, Nene, Nini, Nono. What is the name of the fifth daughter?(为了引导模型逐步推理，加入这一句”Let’s think step by step.”) Mike plays ping pong for 40 minutes. In the first 20 minutes, he scores 4 points. In the second 20 minutes, he scores 25% more points. How many total points did he score? 即使使用 Standard Prompting 而不是 Chain-of-Thought Prompting ，模型在输出时不仅展现了推理能力，而且也能够输出正确答案。 每次调用结果不同，如果在 Prompt 里尝试引导模型推理，符合预期的概率确实更大一些。 我的理解是大模型的效果就像是一个 V 字型，左边是训练生成模型，右边是查询模型，两边都会影响推理效果： 1. 模型自身已进化: 现在的很多大模型，在左边就已经使用更精确的数据、调优解决了我测试的这些 query，因此即使不使用 COT ，效果也非常好了。 2. CoT 提升稳定性: 为了更大概率的拿到想要的输出，还是应当尽量使用 COT 3. CoT 的历史定位: 相比后续出现的策略，COT 最为基础和直接。因为 COT 只使用了 Prompt 来提升效果，因此也是 Prompt Engine 的一种体现。(注：我查了下似乎提出 PE 只比 COT 早了半年) COT 的本质是要引导大模型的推理过程，现在看其实已经是比较普遍的思想了。 现在想想，这也非常符合直觉。就像我们自己计算出错后，会下意识地告诉自己“再仔细想想”。“多想想，按部就班地推演一遍，就会少犯错”——这个简单的道理，在大模型领域被赋予了一个专门的术语：CoT。 相关代码放在 https://github.com/izualzhy/AI-Systems/blob/main/misc/read_paper_cot.py 2. ReAct 从“思考”到“行动”的进化。 2.1. 什么是 ReAct？ ReAct 范式可以看作是 CoT 的一个强大演进。它不仅包含了 CoT 的“推理”（Reasoning）能力，更引入了“行动”（Acting）的概念。 ReAct 的核心机制是一个 Thought -&gt; Action -&gt; Observation 的循环： Thought (思考): 模型根据当前任务和已有信息，分析现状，并规划出下一步需要做什么。 Action (行动): 模型决定执行一个具体的“行动”。这个行动通常是调用外部工具（API），例如进行网络搜索、查询数据库、执行代码等。 Observation (观察): 模型获取“行动”返回的结果。这个结果会作为新的信息，融入到下一轮的“思考”中。 通过这个循环，LLM 能够与外部世界进行交互，获取实时、准确的信息，从而克服其自身知识库静态、可能过时的缺点。 详细论文参考：REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS 2.2. 为什么需要 ReAct？ CoT 极大地增强了模型的推理能力，但它的推理过程是“封闭”的，完全依赖于模型内部的知识。如果遇到以下场景，CoT 就会力不从心： 知识过时： LLM 的知识截止于其训练日期。 需要精确计算： LLM 在数学计算上并不可靠。 需要与外部服务交互： 例如订票、发邮件等。 ReAct 通过引入“行动”，将 LLM 从一个“封闭大脑”变成了一个能够主动探索和获取信息的“智能代理”（Agent），极大地拓展了其应用边界。 这套“思考-行动-观察”的循环，正是 ReAct Agent 的基本工作模式。 2.3. 实践 那么，这篇学术论文的思想在工程实践中是如何应用的呢？主要体现在两个方面： 精心设计的 Prompt：构建一个能引导模型输出“Thought, Action, Action Input”等结构化文本的提示。 外部控制循环：在代码中实现一个循环，用于解析模型输出、调用相应工具、并将工具结果返回给模型，直到任务完成。 我们用 LangChain 框架来直观感受一下： def get_current_weather(location: str): print(f"\n** [函数被调用] location = {location} **\n") return f"{location} 今天天气晴，25°C." tools = [ Tool.from_function( func=get_current_weather, name="get_current_weather", description="获取城市当前天气", handle_parsing_errors=True ) ] # 初始化 Agent # llm = getDoubaoSeed16() llm = getDoubao15VisionLite() # llm = getDeepSeekR1() def v1(): # llm = getDeepSeekR1() agent = initialize_agent(tools, llm, agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION, # agent_type="openai-tools", verbose=True, temperature=0, handle_parsing_errors=True) def run(): print(agent.agent.llm_chain.prompt.template) # 用户提问 result = agent.invoke("请问上海的天气怎么样？") print(result) run() 上述代码输出： Answer the following questions as best you can. You have access to the following tools: get_current_weather(location: str) - 获取城市当前天气 Use the following format: Question: the input question you must answer Thought: you should always think about what to do Action: the action to take, should be one of [get_current_weather] Action Input: the input to the action Observation: the result of the action ... (this Thought/Action/Action Input/Observation can repeat N times) Thought: I now know the final answer Final Answer: the final answer to the original input question Begin! Question: {input} Thought:{agent_scratchpad} &gt; Entering new AgentExecutor chain... Thought: 需要获取上海当前的天气信息，调用 get_current_weather 工具来实现 Action: get_current_weather Action Input: 上海 ** [函数被调用] location = 上海 ** Observation: 上海 今天天气晴，25°C. Thought:I now know the final answer Final Answer: 上海 今天天气晴，25°C. &gt; Finished chain. 可以看到函数被调用，且返回了预期的最终答案。 2.3.1. Prompt ZERO_SHOT_REACT_DESCRIPTION 这个 Agent 内置的 Prompt 模板，完美诠释了 ReAct 的思想： Answer the following questions as best you can. You have access to the following tools: get_current_weather(location: str) - 获取城市当前天气 Use the following format: Question: the input question you must answer Thought: you should always think about what to do Action: the action to take, should be one of [get_current_weather] Action Input: the input to the action Observation: the result of the action ... (this Thought/Action/Action Input/Observation can repeat N times) Thought: I now know the final answer Final Answer: the final answer to the original input question Begin! Question: {input} Thought:{agent_scratchpad} 这个模板通过清晰的指令和格式要求，引导模型遵循 Thought -&gt; Action -&gt; Observation 的路径进行思考和输出。 2.3.2. 循环 class AgentExecutor(Chain): def _call( self, inputs: dict[str, str], run_manager: Optional[CallbackManagerForChainRun] = None, ) -&gt; dict[str, Any]: """Run text through and get agent response.""" # Construct a mapping of tool name to tool for easy lookup name_to_tool_map = {tool.name: tool for tool in self.tools} # We construct a mapping from each tool to a color, used for logging. color_mapping = get_color_mapping( [tool.name for tool in self.tools], excluded_colors=["green", "red"] ) intermediate_steps: list[tuple[AgentAction, str]] = [] # Let's start tracking the number of iterations and time elapsed iterations = 0 time_elapsed = 0.0 start_time = time.time() # We now enter the agent loop (until it returns something). while self._should_continue(iterations, time_elapsed): next_step_output = self._take_next_step( name_to_tool_map, color_mapping, inputs, intermediate_steps, run_manager=run_manager, ) if isinstance(next_step_output, AgentFinish): return self._return( next_step_output, intermediate_steps, run_manager=run_manager ) intermediate_steps.extend(next_step_output) if len(next_step_output) == 1: next_step_action = next_step_output[0] # See if tool should return directly tool_return = self._get_tool_return(next_step_action) if tool_return is not None: return self._return( tool_return, intermediate_steps, run_manager=run_manager ) iterations += 1 time_elapsed = time.time() - start_time output = self._action_agent.return_stopped_response( self.early_stopping_method, intermediate_steps, **inputs ) return self._return(output, intermediate_steps, run_manager=run_manager) 循环的过程在上述代码里，遵循调用大模型 -&gt; 解析大模型返回结果(action/final answer) -&gt; 执行 -&gt; 根据执行结果继续调用.... _should_continue里传入了迭代次数、消耗时间，避免陷入循环一直不返回。 解析返回结果： class MRKLOutputParser(AgentOutputParser): def parse(self, text: str) -&gt; Union[AgentAction, AgentFinish]: """Parse the output from the agent into an AgentAction or AgentFinish object. Args: text: The text to parse. Returns: An AgentAction or AgentFinish object. Raises: OutputParserException: If the output could not be parsed. """ includes_answer = FINAL_ANSWER_ACTION in text regex = ( r"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)" ) action_match = re.search(regex, text, re.DOTALL) if action_match and includes_answer: ... 执行工具的调用栈： AgentExecutor._take_next_step -&gt; AgentExecutor._iter_next_step -&gt; AgentExecutor._perform_agent_action -&gt; BaseTool.run -&gt; Tool._run -&gt; 自定义函数 由于大模型的框架都在快速迭代(换句话说不成熟😄)，就不深入展开代码了。理解概念吧。-_-   2.4. 实践中的困惑 理论很丰满，但现实很骨感。实际上，我第一次运行上述代码时，花费了数小时也未能成功。 我发现，代码能否跑通，极度依赖所选择的 LLM。当我使用 Doubao-Seed-1.6 或 DeepSeek-R1 时，遇到了各种问题： 模型返回结果格式错乱，比如同时输出了 Final Answer 和 Action，导致框架无法判断是否应结束循环。 模型“自作主张”，不调用我定义的 get_current_weather 工具，反而直接输出一段它自己“知道”的关于上海天气的陈旧信息。 直到我换成了 Doubao-1.5-vision-lite 模型，一切才顺利运行。这引人深思：难道模型越高级，反而越不听话，越不遵守 Prompt 的指令了？ 这让我产生了更深层次的理解： 1. ReAct 的本质：大模型本质是概率性的，无法保证100%准确。而现实世界需要确定性。ReAct 的出现，就是为了解决这个��盾：通过引入“工具”这个确定性组件，来弥补大模型的短板。而 Prompt 就是我们让模型学会使用工具的“说明书”。 2. 脆弱的控制链：目前的 Agent 实现对 `模型 + Prompt` 的组合高度敏感。更换一个模型，或者稍微修改一下 Prompt，都可能导致整个控制链条的崩溃。这暴露了当前 Agent 技术的脆弱性。 3. 农场里的科学家：我们现在通过精心设计的 Prompt 来控制大模型的行为，其原理更像是通过大量实验观察到的经验规律，而非精确的理论指导。这让我感觉，我们就像一群“农场鸡舍里的科学家”，试图理解并引导一群我们并未完全掌握其内在机理的“鸡”。 4. 工具的演进：ReAct 引入工具，最初可能只是为了获取更准确、更实时的信息。但这个思想一旦被打开，我们的想象力便不再局限于此，而是希望用 Agent 去完成更复杂、更主动的任务，这催生了当前百花齐放的 Agent ��术浪潮。 注：相关代码在 https://github.com/izualzhy/AI-Systems/blob/main/misc/read_paper_react.py 3. 总结：从思想到行动的演进 回顾这两篇里程碑式的论文，我们可以看到一条清晰的技术演进路径： Standard Prompting：直接问答，完全依赖模型自身知识。 Chain-of-Thought (CoT)：通过引导模型思考中间步骤，提升复杂推理的可靠性。这是内部思维的优化。 ReAct：将 CoT 的“思考”与“行动”（使用工具）相结合，让模型能与外部世界交互，从“思考者”进化为“行动者”。这是思维与实践的结合。 这个过程，是从一个封闭的“知识库”到一个能够进行“研究”和“实践”的初级 Agent 的演变。我在实践中遇到的问题，或许也是当前 Agent 开发领域正在努力解决的核心问题： 如何在给予模型强大推理能力的同时，确保它能可靠、可控地遵循我们设计的流程和规则，在需要时放下“身段”，谦虚地使用工具]]></summary></entry></feed>