之前在折腾Agent的时候,我一直卡在一个问题上:ReAct这种固定循环虽然稳定,但遇到复杂推理任务时,模型总在“想”和“做”之间来回横跳,准确率上不去。后来读到一篇关于self-discover Agent的研究工作,它解决的核心问题不是“怎么让模型更会推理”,而是“怎么让模型自己找到一套适合当前任务的推理方案”。这篇文章是我从理解到落地的一个完整复盘,适合已经接触过CoT、ReAct,想进一步提升Agent复杂任务处理能力的读者。
self-discover的设计思路乍一听反直觉:既然我们已经有了大量人工设计的提示词模板,为什么还要让模型自己去“发明”一套推理方案?但实际体验下来你会发现,固定模板在面对跨领域任务时非常脆弱。同样的CoT格式,在数学题上效果好,换到常识推理上就明显拉胯。self-discover换了个角度——与其猜哪个模板有效,不如让模型先审视当前任务,再从一组专家设计的推理模块里挑出合适的组合,最后按这个组合去执行。整个过程分成UNIVERSAL、DISCOVER、REUSE三个阶段,本质上是在“推理之前先做推理方案设计”。
下面我把这套机制掰开揉碎讲清楚,包括核心原理、完整流程、提示词写法、Python实现和成本测算,最后会把自己踩过的坑和调优经验一并整理出来。
1. 从“人工设计提示词”到“模型自主发现推理方案”
1.1 self-discover Agent到底在解决什么问题
先说一个很多人容易忽略的事实:大模型本身的推理能力其实一直在增长,但提示方式会严重限制这种能力的发挥。同样的模型,用不同提示词跑一道多步逻辑题,结果可能天差地别。传统做法是由人来设计提示模板,比如思维链CoT、自洽性CoT-SC、从易到难Least-to-Most,都是专家根据经验总结出来的“标准答案”。问题是,这些模板都是通用的,没有针对具体任务调整,遇到边界情况就容易失效。
self-discover想解决的,正是“如何为当前任务自动生成一套定制推理方案”的问题。它不是教模型“怎么想”,而是让模型先“想好怎么想”。这句话听着拗口,但实际价值非常大。比如你给Agent一个复杂的商业分析任务,如果直接让它回答,它可能会漏掉关键约束;如果套用CoT模板,它可能机械地分步骤写一堆废话。而self-discover会让模型先从模块库中选中“关键因素分析”“批判性思维”“逆向推理”等模块,再组合成一套针对该任务的分析框架,最后才真正开始推理。这个框架不是人写的,是模型根据任务特点自己拼出来的。
我在实际使用中最大的感受是,self-discover尤其适合那些“任务类型模糊”的场景。如果你已经能明确判断任务属于哪一类(比如就是一道数学应用题),直接上CoT可能更快;但如果任务是开放式的,或者混合了多种推理类型,self-discover的定制方案优势就很明显。
1.2 核心设计思想:从“生成”到“发现”
这里有一个关键的设计细节值得单独说:self-discover的作者把过程叫“发现”,而不是“生成”。这个词的差异不是咬文嚼字,而是核心区别。如果直接让模型“生成推理方案”,模型很可能自由发挥出一堆不可控的内容,输出格式五花八门,方案质量也无法保证。但如果让它从一个固定集合中去“选择”模块,再对这些模块做任务适配,方案的合理性就有基本保障。
打个比方,前者是让一个厨师凭空发明一道菜,后者是给厨师一个食材库和烹饪技法库,让他在约束条件下自由搭配。后者显然更可控。self-discover用到的“食材库”就是一组精选的推理模块,论文里整理了29个,涵盖常见的推理策略,比如批判性思考、逐步推理、逆向推理、分治法、类比推理等等。模型在发现阶段做的,就是从这个库里选几个模块出来,然后针对当前任务做改写和组合,最终形成一份结构化的推理计划。
这种“先约束再组合”的思路,后来被很多Agent框架吸收,比如有些工作流引擎里所谓的“动态规划”,本质上也是让模型基于预设组件库拼装方案。区别在于self-discover更纯粹,它的输出直接就是一段结构化的推理步骤清单,不需要额外解析或转换。
1.3 和主流Agent方案的定位对比
为了把self-discover放到合适的位置,我整理了几种常见方案的对比,并标注各自的适用场景。以下是基于公开文献和开源社区实践的综合信息:
| 方案 | 核心思想 | 主要优势 | 主要局限 |
|---|---|---|---|
| CoT | 让模型逐步思考 | 简单易用,效果稳定 | 模板固定,跨任务泛化差 |
| CoT-SC | 多次采样取多数票 | 提升答案可靠性 | token成本成倍增加 |
| Least-to-Most | 从易到难分解子问题 | 适合复杂分解任务 | 需要人工设计分解策略 |
| ReAct | 推理与行动交替循环 | 适合工具调用型任务 | 复杂推理时路径不稳定 |
| Plan-and-Solve | 先规划再执行 | 分工明确,可控性强 | 规划阶段仍是人工模板 |
| self-discover | 模型自动发现推理方案 | 方案定制化,跨任务鲁棒性好 | 发现过程消耗额外token |
从表里能看出来,self-discover最独特的地方在于,它把“方案设计”这件事也交给模型完成,而不只是让模型按既定方案执行。单看这一点,它更像一个“元任务处理框架”:先解决“怎么做”,再解决“做什么”。这个定位让它天然适合嵌入更大的Agent系统中,作为任务规划模块或前置分析模块。
实际对比下来,在需要多步推理的常识问答和数学题场景,self-discover的准确率通常比普通CoT高出一截;在工具调用型任务上,它和ReAct的差距没有那么大,但有时能通过更好的任务分解来减少不必要的工具调用次数。下面第二部分把机制拆开看。
2. 核心机制拆解:29个推理模块与三阶段流程
2.1 29个推理模块是什么
self-discover最吸引我的地方,就是这套“推理模块库”。它不像ReAct那样只有“思考-行动-观察”一个循环,而是提供了29个可复用的原子推理策略。这里说几个典型的模块,方便建立直观印象:
- 批判性思考模块:要求模型“质疑信息、寻找反例、评估论证强度”,适合需要严谨判断的任务,比如合同条款分析。
- 逐步推理模块:即常见的“一步一步推导”,适合数学题和多步逻辑题。
- 逆向推理模块:从目标倒推需要的条件,适合规划类任务,比如“从B逆推A”。
- 分治模块:把大问题拆成多个独立子问题分别求解,适合复杂系统设计。
- 类比推理模块:将当前问题映射到已知类似问题,适合新领域的快速上手。
需要强调的是,这29个模块不是凭空拍脑袋定的,而是参考了认知心理学和早期提示工程研究中反复出现的高频策略。它们的存在给模型的“发现”过程划定了一个高效搜索空间。如果让模型完全自由发挥,搜索空间是无限的,结果不可控;如果只给少量模块,表达能力又不够。29个是在覆盖面与选择成本之间比较平衡的取值。
在实际落地时,我建议不要把这29个模块当成不可变配置。完全可以根据你的业务场景做裁剪和新增。比如你做数学题为主,可以增加“设未知数模块”“约束列举模块”;你做法律文本处理,可以增加“找前提模块”“量责推理模块”。模块库的意义在于提供一个高质量起点,而不在于固定死。
2.2 UNIVERSAL阶段:定义搜索空间
整个self-discover流程的第一步,我习惯上叫“公布菜单”。模型在这一步的任务非常简单:读一遍所有模块的简短描述,作为后续选择的候选集。论文里把这一步叫UNIVERSAL,因为它不针对具体任务,只是让模型熟悉可用的推理组件。
这里有个细节:UNIVERSAL阶段不是在每轮都重新执行。通常的做法是提前把29个模块的描述作为系统提示的一部分,或者只在第一轮注入一次。因为模块描述是固定不变的,重复注入既浪费token,也没有额外收益。我在实现时,就是把这些描述硬编码在代码里,只在DISCOVER阶段的提示词中引用它们。
说句题外话,早期版本里我看到有些复现代码直接把29个模块描述拼接成一个大字符串,然后每一轮都发一遍,token消耗非常可观。这个问题后面在成本优化部分会再讲。UNIVERSAL阶段本身不需要模型输出任何东西,主动权完全在代码侧,只要你把模块库准备好,它就是一个静态上下文。
2.3 DISCOVER阶段:选择、适配与实现
DISCOVER是self-discover的核心环节,细分成三个小步骤:SELECT(选择)、ADAPT(适配)、IMPLEMENT(实现)。这三个步骤可以一次调用完成,也可以分三次调用,效果上各有优劣,后面会对比说明。
SELECT环节,模型从29个模块中挑选与当前任务最相关的若干个模块,并且要给出选择理由。这一步的提示词可以这样写:
task = "某公司营收同比下降15%,但用户增长20%,请你分析可能的原因并提出应对策略。" select_prompt = f""" 以下是可用的推理模块列表: {module_descriptions} 针对下面的任务,请选择最相关的推理模块(最多选5个,最少选2个), 并简要说明每个模块适用于该任务的哪些方面。 任务:{task} 输出格式: 模块名:选择理由 """ADAPT环节,模型把选出的通用模块改写成当前任务语境下的具体推理指令。比如“逐步推理”模块经过适配后,可能变成“请从财务、市场、运营三个维度分步骤拆解营收下滑的可能原因”。这一步的目的是把通用策略变成可执行的任务指令,减少推理时模型理解上的二义性。
IMPLEMENT环节,模型把适配后的模块进一步整理成一份结构化的执行计划,通常是一个带编号的步骤清单。这个清单就是后续REUSE阶段的行动指南。把三个环节合并成一次提示词调用时,模型会直接输出“选择了哪些模块 -> 适配后的指令 -> 完整执行计划”,内容较长,但总体逻辑清晰。分开调用更利于控制每一步的输出质量,但会额外增加两到三次API往返。我刚开始实现时选择分开,跑通了再合并优化。
2.4 REUSE阶段:用发现的方案完成推理
最后一个阶段是REUSE,模型严格按IMPLEMENT阶段生成的执行计划一步步推理,得出最终答案。这里有个常见误区:REUSE阶段不是简单地把原问题重新发给模型,而是要把任务本身、执行计划、当前进度等信息一并作为上下文,让模型“照着剧本走”。如果只给计划不给任务,模型容易丢失原始目标;如果只给任务不给计划,那整个self-discover就白做了。
REUSE阶段的提示词设计有几个要点。第一,明确告知模型“以下是针对本任务的推理方案,请严格按照方案执行”。第二,方案中每一步最好单独触发一次模型调用,而不是让模型一次性输出完整答案。这样可以及时检查中间结果是否正确,一旦某一步走偏能尽早纠正。第三,保留中间步骤的输出,方便回溯排错。这个阶段的输出结构通常是:当前步骤、执行结果、下一步操作。
这里补一句我在实际项目中踩到的教训:REUSE阶段的“严格执行”和Agent自主性之间存在张力。如果模型觉得自己有更好方案,可能会跳出计划自行发挥。这不一定坏事,但如果不加控制,整个流程就退化成普通CoT了。我会在提示词中加一句“如果某一步无法执行,请说明原因,不要跳过”,既能保留灵活性,又不会彻底脱轨。
3. 实操落地:从零实现一个可跑的self-discover流程
3.1 环境准备与模型选型
要跑通self-discover,其实不需要很重的框架。只要有大模型API的调用能力,加上基本的Python环境就够了。我这边用的是Python 3.10,依赖只用了requests和json这两个标准库,没有引入额外的Agent开发框架。如果你用的是OpenAI兼容接口,代码几乎可以直接复用;用其他厂商的API,只要改成对应的请求格式就可以。
模型选型上,建议用推理能力中等偏上的模型。self-discover的逻辑依赖模型对任务和模块的理解力,如果模型太弱,连SELECT阶段都很难选出合理的模块。我实测下来的经验是:上下文理解能力明显强的模型,跑self-discover才有收益。如果模型水平不太够,就别硬套,直接用CoT反而更稳。
温度参数建议分阶段设置。DISCOVER阶段可以用temperature=0.2,让选择结果尽量稳定;REUSE阶段如果是数学或逻辑题,也建议低温甚至用0;如果是创意类任务,可以适当提高到0.4到0.7。注意,temperature=0并不等于绝对随机,但能显著降低输出波动。
3.2 核心数据结构:模块库的设计
模块库是self-discover的地基。我建议把每个模块定义为一个字典,至少包含模块名和描述。描述要简练准确,太长了模型在SELECT阶段容易注意力涣散,太短了又表达不清。以几个模块为例:
modules = [ { "name": "critical_thinking", "description": "质疑信息,寻找反例,评估论证强度。适合信息可信度判断与严谨论证的任务。" }, { "name": "step_by_step", "description": "将推理过程分成清晰的小步骤,逐步推进,每一步都基于上一步的结论。适合数学题与多步逻辑题。" }, { "name": "backward_reasoning", "description": "从目标出发逆向推导,思考要达到目标需要哪些条件,再反推当前需要做什么。适合规划与归因任务。" }, { "name": "divide_and_conquer", "description": "将复杂问题拆分成多个相对独立的子问题,分别求解后再汇总结果。适合大型复杂任务。" } ]整个模块库我放在一个Python模块里,运行时load进来,方便增删改。这里有个技巧:模块描述可以按“策略-适用场景”的结构来写,模型在选择时信息更充分。纯描述性的模块,模型可能不太清楚什么时候该用。
3.3 三阶段提示词的具体写法
提示词是self-discover的关键。我把三个阶段提示词的骨架贴出来,大家可以直接参考。首先是SELECT阶段的提示词:
select_prompt = f""" 给定以下推理模块列表: {json.dumps(modules, ensure_ascii=False)} 请针对下面这个任务,选出最适合的推理模块(3到5个)。要求: 1. 说明每个模块为什么适合这个任务; 2. 按优先级从高到低排列。 不要执行任务,只做模块选择。 任务:{task} """接着是ADAPT阶段,注意这里需要把SELECT阶段的输出作为输入传入:
adapt_prompt = f""" 你选择了以下推理模块来解决任务:{selected_modules} 任务:{task} 请将这些通用模块改写成专门针对该任务的推理指令。 改写后的每条指令应当可以直接执行,不依赖通用模块的抽象描述。 输出格式:每条指令一行,以数字开头。 """最后是IMPLEMENT阶段的提示词:
implement_prompt = f""" 基于以下适配后的指令,生成一份结构化的执行计划。 计划中每一步都应包含:目标、输入、执行动作、输出判断标准。 最多不要超过8步。 执行计划:{adapted_modules} 任务:{task} """这里说一个容易忽略的点:IMPLEMENT生成的执行计划,一定要包含“输出判断标准”这一项。我最初写的时候忽略了,结果REUSE阶段的模型经常不知道该在什么时候结束,有时会无限递归下去或者重复完成某一步。现在所有执行步骤都带明确的终止条件,模型跑起来明显更省心。
3.4 一个可运行的Python示例
下面给出一个简化但完整可跑的示例,省略API调用细节,保留主要流程。
import json import requests API_URL = "your_api_endpoint" API_KEY = "your_api_key" def llm_call(prompt, temperature=0.2): resp = requests.post( API_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json={ "model": "your_model_name", "messages": [{"role": "user", "content": prompt}], "temperature": temperature } ) return resp.json()["choices"][0]["message"]["content"] task = "某公司营收同比下降15%,但用户增长20%,请你分析可能的原因并提出应对策略。" # Step 1: UNIVERSAL (modules are pre-loaded as context) select_prompt = f"给定模块列表:{json.dumps(modules, ensure_ascii=False)}...任务:{task}...只做选择" selected = llm_call(select_prompt, temperature=0.2) print("SELECT 输出:\n", selected) # Step 2: ADAPT adapt_prompt = f"基于选定模块{selected}改写任务指令:{task}" adapted = llm_call(adapt_prompt, temperature=0.2) print("ADAPT 输出:\n", adapted) # Step 3: IMPLEMENT implement_prompt = f"基于指令{adapted}生成结构化执行计划,任务:{task}" plan = llm_call(implement_prompt, temperature=0.2) print("IMPLEMENT 输出:\n", plan) # Step 4: REUSE reuse_prompt = f"""任务:{task} 请严格按照以下执行计划完成推理: {plan} 现在开始执行第1步,并输出结果。""" final_answer = llm_call(reuse_prompt, temperature=0) print("REUSE 输出:\n", final_answer)真机跑起来,REUSE阶段通常不会一步到底。更稳妥的做法是循环执行:每次只走执行计划中的一步,把当前进度拼到上下文里,再请求下一步。这个过程类似ReAct的循环,但循环的“剧本”已经由IMPLEMENT阶段提前定好了,所以模型每一步的任务非常明确,不容易跑偏。
3.5 成本测算:一次完整调用要花多少token
token成本是self-discover绕不开的话题。我以一次简单任务为例,做了一个粗粒度测算,方便大家心里有数。假设模块列表约600 token,SELECT阶段输入约900 token、输出约200 token;ADAPT阶段输入约900 token、输出约300 token;IMPLEMENT阶段输入约1200 token、输出约300 token;REUSE阶段如果走5步,每步输入约1500 token、输出约200 token。
总消耗粗算下来,输入输出加起来大约在1万token左右。如果是商用API,按市面上常见的中大模型定价折算,单次任务成本大约在几毛钱到几块钱之间,取决于你用的模型。如果任务本身很简单,这个成本显然偏高,这也是很多团队最终只在复杂任务场景启用self-discover的原因。
关于成本优化,有三个实用方法。第一,把UNIVERSAL阶段从每次调用中移除,模块描述只在第一次请求时注入,后面对话中截断,最高可以省掉600到800 token的重复开销。第二,如果模型允许缓存系统提示,就把模块列表作为系统提示缓存,多次任务之间复用。第三,对SELECT阶段做长尾过滤,如果前一次运行发现某个模块被选中的概率极低,可以直接从库里暂时移掉,收敛令牌用量。
4. 常见问题、踩坑记录与调优建议
4.1 效果不佳时的排查清单
如果用了self-discover反而效果变差,先别急着下结论,按下面这个顺序逐项排查,能解决大部分问题。
第一,检查模型能力是否过关。前面提过,self-discover对模型推理能力的消耗比CoT大,模型太弱时SELECT就是瞎选,ADAPT就会瞎写,最后整个方案是垃圾。如果你用的模型跑普通CoT都很吃力,直接用self-discover大概率是浪费钱。
第二,检查模块库是否贴合领域。默认29个模块偏通用,遇到垂直领域(比如医学、金融、法律)时,可能没有覆盖关键推理策略。我建议建立领域模块库,每新增一个任务类型,就把有效的策略补充进去,几个月后你会有自己专属的高质量模块集。
第三,检查REUSE阶段是不是真的“严格按方案执行”。一个很常见的翻车原因是模型在REUSE阶段自己改方案,尤其在执行计划比较长的时候。建议在每一轮REUSE的提示词开头加一句“不要修改既定方案,若某步无法完成,请说明原因并停在原步骤”。实测这句提示能显著减少自行发挥的概率。
第四,检查输出格式解析是否稳定。模型输出的步骤编号可能不统一,建议在提示词中明确输出格式,例如“每一步以[STEP i/START]开头,以[STEP i/END]结尾”。这样程序解析更稳,断点续跑也更容易。
4.2 token与延迟优化:把self-discover用在刀刃上
做工程的人都懂,一个方案效果好但太慢太贵,就很难上生产。self-discover的“发现”部分天然多出两三次调用,这是它比ReAct慢的根本原因。我的优化思路有三个。
第一,对任务先做“难度分级”。简单任务直接CoT,中等任务用self-discover,复杂任务再加多重采样。简单粗暴,但非常有效。判断标准可以做成规则:任务描述中包含“分析、评估、规划、对比”这类词,或者人工标记为高难度时,才进入self-discover分支。
第二,把DISCOVER阶段从“三个子环节”合并成“一次调用”。合并后,模型输出会变得不可控一些,但配合严格的JSON输出约束,实际使用中效果不差太多,成本和时间却砍掉一半。我在生产项目里就用的合并版本。做法是在提示词里写清楚“按三级标题输出:SELECT结果、ADAPT结果、IMPLEMENT结果”,再用正则切分。
第三,利用缓存和异步并行。DISCOVER阶段的三次调用之间没有严格依赖,可以异步并发请求,整体延迟从三次串行缩短到接近一次。当然这个需要模型服务端支持并发,如果你走的是自己搭的推理服务,并发收益更明显。
4.3 什么时候不该用self-discover
这里说点反常识的:self-discover不是万金油,有些时候它反而拖后腿。比如简单的知识问答、翻译、摘要这类任务,模型本身就不需要复杂推理方案,套个CoT或者直接回答就足够,强行self-discover只会增加延迟和成本。
还有一类是“高度程序化”的任务。比如Agent需要调用固定的工具链:查数据库、执行代码、返回结果。这种流程本来就固定,self-discover只会引入不确定性,方案每次跑得还不一样。更好的做法是用确定性工作流编排,该走哪个API就写死在流程里。
另外,如果你的线上服务对首字延迟要求苛刻,self-discover的额外往返会很难接受。这种情况下建议把发现阶段放到离线异步任务里,提前生成好各种任务类型的推理方案,在线时直接复用。等于“离线发现、在线执行”,既保留了self-discover的优点,又不拖慢线上响应。
4.4 与其它Agent框架的搭配思路
self-discover其实很适合作为Agent架构中的一个“规划器”存在。常见的做法是:Agent拿到用户任务后,先用self-discover生成执行计划,再交给ReAct或Plan-and-Execute执行环境去跑。这相当于把“任务规划”和“任务执行”彻底分离,规划靠模型自省,执行靠工具调用,各司其职。我自己测试过几种搭配,整体最优的组合是这样:
- 第一步:self-discover生成结构化计划,计划里显式标注“哪一步需要调用外部工具”。
- 第二步:ReAct循环按计划执行,碰到工具调用就触发,工具返回结果之后就更新上下文。
- 第三步:所有步骤结果收集完,交给一个汇总模型做最终输出。
这个组合比纯ReAct的准确率高,又比纯self-discover更实用,因为很多任务不调用工具根本完不成。实现难度也不大,只是把原来ReAct的“思考-行动-观察”循环里,把“思考”部分替换成“按计划的步骤思考”,逻辑清晰,排错也容易。
最后再分享一个基于这个思路做的小验证:我拿一份广泛流传的多步逻辑测试集做了对比(自行复现,非官方数据),纯ReAct的通过率大概在某个基础水位,而ReAct加上self-discover前置规划后,几类子任务的通过率都有明显提升,尤其是在需要多轮信息整合的题目上。代价就是延迟长了大约一倍,但对正确率敏感的场景完全可以接受。
用一句话概括我的体会:self-discover不是用来替代所有Agent方案的银弹,它解决的是“任务规划不匹配”这个具体问题。当你发现模型推理结果忽好忽坏、现有提示词模板在新任务上泛化性不足时,不妨把self-discover加到你Agent的规划环节里试试。它的价值不在于惊艳,而在于稳定地帮你生成一条更适合当前任务的思考路径,这在复杂任务的实战里尤其宝贵。