"""
=============================================================================
第 4 讲 · 代码 05：各种思维链提示的比较（模拟器）—— 准确率、成本、规模与投票
=============================================================================

对应讲义：lectures/lecture04_chain_of_thought.md §3、§4、§6

【这个脚本要回答的问题】
    在 reasoner_sim 的模拟推理者上（每种行为都是一个显式的旋钮，见 reasoner_sim.py 开头）：
    1. 直接回答、少样本思维链、零样本思维链、先计划再求解、由程序逐步分解、self-consistency：
       题目步数从 1 到 8，各自的准确率和**成本**（输出 token、调用次数）是多少？（演示 1）
    2. 模型规模：思维链为什么在小模型上没用甚至有害、在大模型上才有用？（演示 2）
    3. self-consistency 能涨到哪里为止？系统性错误的比例决定了它的上限。（演示 3）
    4. 一步就能答的题，思维链还有用吗？（演示 4）

【这个演示能 / 不能说明什么】
    结果完全由旋钮决定，**不能**说明真实模型的数字；它的用处是把"为什么"讲清楚。真实模型的数字见讲义引用的论文。

【运行】python3 05_prompting_variants_sim.py     （约 15 秒）
【依赖】只用标准库；import 同目录的 reasoner_sim.py，前置讲 P2 的 llm_api.py（token 计数），第 1 讲的 console.py。
"""

import os
import random
import sys
from collections import Counter

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lecture01"))
from console import print_header, print_observations, print_table  # noqa: E402
from reasoner_sim import METHODS, SimReasoner, count_tokens, make_problem  # noqa: E402

N = 300
KS = [1, 2, 4, 6, 8]
LABEL = {"direct": "直接回答", "cot": "少样本思维链", "zero_cot": "零样本思维链", "plan": "先计划再求解",
         "decompose": "程序逐步分解"}


def problems(k, n=N, seed=0):
    rng = random.Random(1000 * k + seed)
    return [make_problem(rng, k) for _ in range(n)]


def run(sim, ps, method):
    """一种方法在一组题上的 (准确率, 平均输出 token, 平均调用次数)。"""
    ok = toks = calls = 0
    for p in ps:
        text, ans = sim.solve(p, method)
        ok += ans == p["answer"]
        toks += count_tokens(text)
        calls += len(p["steps"]) if method == "decompose" else 1
    return ok / len(ps), toks / len(ps), calls / len(ps)


def run_sc(sim, ps, m):
    """self-consistency：m 条少样本思维链（采样编号 1..m）的多数投票。"""
    ok = toks = 0
    for p in ps:
        outs = [sim.solve(p, "cot", s) for s in range(1, m + 1)]
        votes = Counter(a for _, a in outs)
        ok += votes.most_common(1)[0][0] == p["answer"]
        toks += sum(count_tokens(t) for t, _ in outs)
    return ok / len(ps), toks / len(ps), m


def demo1():
    """演示 1：方法 × 步数（large）。"""
    print_header("演示 1：SimReasoner-large 上的各种方法（每种步数 300 道题）——准确率；最后两列是 8 步题的成本")
    sim = SimReasoner("large")
    rows, res = [], {}
    for method in METHODS + ["sc5", "sc10"]:
        accs = []
        for k in KS:
            ps = problems(k)
            r = run_sc(sim, ps, int(method[2:])) if method.startswith("sc") else run(sim, ps, method)
            accs.append(r[0])
            res[(method, k)] = r
        label = LABEL.get(method, f"self-consistency @{method[2:]}")
        rows.append([label] + [f"{100 * a:.0f}%" for a in accs] + [f"{res[(method, 8)][1]:.0f}", f"{res[(method, 8)][2]:.0f}"])
    print_table(["方法"] + [f"{k} 步" for k in KS] + ["输出 token（8 步）", "调用次数（8 步）"], rows)
    d4, c4, p4 = res[("direct", 4)][0], res[("cot", 4)][0], res[("plan", 4)][0]
    print_observations([
        f"直接回答在 1–2 步时还行，4 步就跌到 {100 * d4:.0f}%：'心算'能可靠完成的步数有限（旋钮 cap）。"
        f"少样本思维链在 4 步时是 {100 * c4:.0f}%——把每一步写出来，每一步只需要算一次简单的运算。",
        f"零样本思维链（'Let's think step by step'）比少样本略低：没有示例规定格式，偶尔不推理、推理也稍不规范。"
        f"Kojima et al.（2022）报告这句话把 InstructGPT 在 MultiArith 上的零样本准确率从 17.7% 提到 78.7%。",
        f"先计划再求解（4 步 {100 * p4:.0f}%）减少了'漏掉一步'的错误；Wang et al.（2023，Plan-and-Solve）报告它在多个算术基准上"
        "优于零样本思维链。",
        "程序逐步分解和 self-consistency 最准，但成本也最高：逐步分解每一步一次调用（8 步就是 8 次），"
        "self-consistency @10 的输出 token 是单条思维链的 10 倍。**更准的方法几乎总是更贵**——要按任务的价值决定花多少。",
    ])


def demo2():
    """演示 2：模型规模。"""
    print_header("演示 2：模型规模 × 方法（1 步和 4 步的题）")
    rows, res = [], {}
    for size in ["small", "medium", "large"]:
        sim = SimReasoner(size)
        row = [size, f"e = {sim.k['e_step']}, 额外的'不合逻辑' = {sim.k['illogical']}"]
        for k in (1, 4):
            ps = problems(k)
            for method in ("direct", "cot", "zero_cot"):
                res[(size, k, method)] = run(sim, ps, method)[0]
                row.append(f"{100 * res[(size, k, method)]:.0f}%")
        rows.append(row)
    print_table(["规模", "旋钮", "1 步 · 直接", "1 步 · 少样本 CoT", "1 步 · 零样本 CoT", "4 步 · 直接", "4 步 · 少样本 CoT",
                 "4 步 · 零样本 CoT"], rows)
    print_observations([
        f"small：1 步题上思维链（{100 * res[('small', 1, 'cot')]:.0f}%）比直接回答（{100 * res[('small', 1, 'direct')]:.0f}%）**更差**；"
        f"4 步题上两者都接近 0。小模型写出的推理'流畅但不合逻辑'，每多写一步就多一次出错的机会。",
        f"large：4 步题上思维链 {100 * res[('large', 4, 'cot')]:.0f}%，直接回答 {100 * res[('large', 4, 'direct')]:.0f}%。",
        "这正是 Wei et al.（2022）的发现：思维链提示对小模型没有帮助、甚至有害，在约 1000 亿参数的模型上才显著有效——"
        "他们把它称为一种**涌现**能力。本讲 02 号脚本的迷你 GPT 上，完整句子的思维链失败，是这个现象的一个真实的小例子。",
        "零样本思维链对规模更敏感：小模型听到 'step by step' 往往并不真的开始推理（旋钮 p_zero_cot）。",
    ])


def demo3():
    """演示 3：self-consistency 的上限由系统性错误决定。"""
    print_header("演示 3：self-consistency 能涨到哪里？——改变'系统性误解'的比例（large，4 步题）")
    ps = problems(4)
    rows = []
    res = {}
    for mis in [0.0, 0.06, 0.15, 0.3]:
        sim = SimReasoner("large", misconception=mis)
        greedy = run(sim, ps, "cot")[0]
        accs = [run_sc(sim, ps, m)[0] for m in (1, 5, 15, 40)]
        res[mis] = (greedy, accs)
        rows.append([f"{100 * mis:.0f}%", f"{100 * greedy:.0f}%"] + [f"{100 * a:.0f}%" for a in accs] + [f"{100 * (1 - mis):.0f}%"])
    print_table(["系统性误解的比例", "贪心思维链", "投票 @1", "@5", "@15", "@40", "参考：1 − 比例"], rows)
    g0, a0 = res[0.0]
    print_observations([
        f"没有系统性误解时，投票 @40 到 {100 * a0[-1]:.0f}%，远高于贪心的 {100 * g0:.0f}%：随机的算错发生在不同的步骤、错成不同的数，"
        "多数投票把它们投掉了。",
        "贪心思维链比单条采样（投票 @1）还低：模拟器里最常被选中的解题路径（权重 0.5）恰好最容易出错。概率最大的**路径**"
        "未必通向概率最大的**答案**——这是 Wang et al.（2023）用'对推理路径求和'来解释 self-consistency 的核心。",
        "系统性误解每次采样都在，投票修不掉：误解的比例是多少，投票的上限就大约差多少（最后一列；略有出入，是因为误解所在的那一步"
        "偶尔恰好被漏掉或被另一个算错抵消）。04 号脚本里领导人做桥的那些题，就是迷你 GPT 的'系统性误解'。",
        "所以 self-consistency 的收益是一个经验问题：它取决于模型的错误有多'随机'。在真实模型上，Wang et al. 报告的收益"
        "在不同任务、不同模型上从几个百分点到近 20 个百分点不等。",
    ])


def demo4():
    """演示 4：一步就能答的题。"""
    print_header("演示 4：一步就能答的题（large）——思维链还值得吗？")
    sim = SimReasoner("large")
    ps = problems(1)
    rows = []
    for method in ("direct", "cot", "zero_cot", "plan"):
        acc, toks, _ = run(sim, ps, method)
        rows.append([LABEL[method], f"{100 * acc:.1f}%", f"{toks:.1f}"])
    print_table(["方法", "1 步题的准确率", "平均输出 token"], rows)
    print_observations([
        "一步题上，思维链和直接回答的准确率差不多，输出 token 却多了好几倍。",
        "Sprague et al.（2024，《To CoT or not to CoT?》）汇总了一百多篇论文、并在 14 个模型上做了实验，发现思维链的收益"
        "**主要集中在数学和符号推理**上；在常识、知识问答等任务上收益很小——那里的瓶颈是'知不知道'，不是'会不会算'。",
        "实践上：先问自己'这道题需要几步？'。一步就能答的问题（查事实、分类、抽取），通常不需要让模型长篇推理。",
    ])


def main():
    demo1()
    demo2()
    demo3()
    demo4()


if __name__ == "__main__":
    main()
