"""
=============================================================================
第 4 讲 · 代码 02：教模型先写中间步骤 —— 直接回答、完整句子的思维链、只写中间结果
=============================================================================

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

【这个脚本要回答的问题】
    同样的两跳训练数据（6 种组合、12 个 SFT 国家），回答写成三种格式，训练三个模型：
        direct  ：直接写答案                         "driltese"
        sentence：先写两句完整的推理，再写答案        "grorken is the capital of volel . people in volel speak driltese . answer driltese"
        scratch ：只写下中间结果（那个国家），再写答案  "volel . answer driltese"
    1. 训练过的组合、没训练过的组合，三种格式各能答对多少？要多写多少 token？（演示 1）
    2. 完整句子的思维链错在哪里？（演示 2）

【怎么读输出】
    "中间国家写对"只对两种思维链有意义：回答里写下的国家是不是正确的那个。
    "给出了答案"：回答里有没有 "answer X"（完整句子的思维链有时写着写着就忘了给答案）。

【运行】python3 02_cot_formats.py     （第一次约 30 秒：训练 2 个新模型；之后读缓存，约 15 秒）
【依赖】PyTorch；import 同目录的 cot_lab.py，第 1 讲的 console.py。
"""

import os
import sys

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
import re  # noqa: E402

from cot_lab import (EVAL_COUNTRIES, SFT_COUNTRIES, TARGETS, TEST_COMBOS, TRAIN_COMBOS, WORLD, ask, attr,  # noqa: E402
                     eval_twohop, get_cot_model, render_chat, twohop_question)

NO_LEADER = lambda cs: [x for x in cs if x[0] != "leader"]       # noqa: E731  用首都 / 语言做桥的组合


def demo1(models, tok):
    """演示 1：三种格式 × 训练过 / 没训练过的组合 × SFT 国家 / 测试国家。"""
    print_header("演示 1：三种回答格式（用首都 / 语言做桥的组合；领导人做桥的另列）")
    rows, res = [], {}
    settings = [("训练过的组合 · SFT 国家", NO_LEADER(TRAIN_COMBOS), SFT_COUNTRIES),
                ("训练过的组合 · 测试国家", NO_LEADER(TRAIN_COMBOS), EVAL_COUNTRIES),
                ("没训练过的组合 · 测试国家", NO_LEADER(TEST_COMBOS), EVAL_COUNTRIES),
                ("领导人做桥（没训练过的组合）· 测试国家", [x for x in TEST_COMBOS if x[0] == "leader"], EVAL_COUNTRIES)]
    for fmt, m in models.items():
        for label, combos, cs in settings:
            r = eval_twohop(m, tok, combos, cs, fmt)
            res[(fmt, label)] = r
            rows.append([fmt, label, f"{100 * r['acc']:.1f}%", "–" if fmt == "direct" else f"{100 * r['bridge']:.1f}%",
                         f"{100 * r['answered']:.1f}%", f"{r['tokens']:.1f}"])
    print_table(["格式", "测试集", "最终答对", "中间国家写对", "给出了答案", "平均输出词数"], rows)
    d = res[("direct", settings[2][0])]["acc"]
    s = res[("scratch", settings[2][0])]["acc"]
    se = res[("sentence", settings[2][0])]["acc"]
    ds, ss = res[("direct", settings[1][0])]["acc"], res[("scratch", settings[1][0])]["acc"]
    print_observations([
        f"没训练过的组合上，direct {100 * d:.1f}%，scratch {100 * s:.1f}%：只是让模型**先把中间的国家写下来**，"
        "组合泛化就从'几乎不会'变成'大多会'。写下的国家成了上下文的一部分，第二跳变成了模型本来就会的一跳问题——"
        "思维链的核心作用：把需要在一次前向计算里完成的组合，摊开成几次'写下来、再读回来'。",
        f"训练过的组合、测试国家：direct {100 * ds:.1f}%、scratch {100 * ss:.1f}%。direct 在训练过的组合上并不差"
        "（它可以学到捷径），差距主要出现在没训练过的组合上。",
        f"完整句子的思维链（sentence）却在测试国家上表现很差（没训练过的组合 {100 * se:.1f}%）。它写的推理更'像样'，"
        "可每多写一个词，就多一个出错的机会（演示 2）。Wei et al.（2022）发现思维链提示对小模型（约 100 亿参数以下）"
        "没有帮助、甚至有害，只在约 1000 亿参数的模型上才明显有效——生成一条正确的推理，本身就需要能力。",
        f"代价：scratch 每题多写约 {min(res[('scratch', l[0])]['tokens'] - res[('direct', l[0])]['tokens'] for l in settings[:3]):.0f} 个词，"
        f"sentence 多写 {min(res[('sentence', l[0])]['tokens'] - res[('direct', l[0])]['tokens'] for l in settings[:3]):.0f}–"
        f"{max(res[('sentence', l[0])]['tokens'] - res[('direct', l[0])]['tokens'] for l in settings[:3]):.0f} 个。"
        "真实模型的思维链常常是几百上千个 token，费用和延迟随之增加（讲义 §6）。",
        "领导人做桥：三种格式都接近 0。思维链不能凭空变出模型不知道的事实——第一跳做不到，写多少步都没用。",
    ])
    return res


def demo2(m, tok):
    """演示 2：完整句子的思维链错在哪里。"""
    print_header("演示 2：完整句子的思维链错在哪里（没训练过的组合、测试国家）")
    names = {c.name for c in WORLD.countries}
    same_type = {"capital": {c.capital for c in WORLD.countries}, "language": {c.language for c in WORLD.countries}}
    cats = ["没写推理：直接写了一个国名", "抄错了问题里的实体（写成了别的首都 / 语言）", "其他格式",
            "抄对了，但写成了百科、没给答案", "第二句套用了别的目标（训练过的组合）", "其他错误", "完全正确"]
    count = {k: 0 for k in cats}
    examples = {}
    n = 0
    for b, t in NO_LEADER(TEST_COMBOS):
        target_re = re.compile("^" + TARGETS[t][1].replace("{x}", r"\w+").replace("{a}", r"\w+") + "$")
        for c in EVAL_COUNTRIES:
            q = twohop_question(b, t, c)
            out = ask(m, tok, render_chat(q))
            words = out.split()
            parts = [x.strip() for x in out.split(" . ")]
            entity = c.capital if b == "capital" else c.language
            n += 1
            if words and words[0] in names and len(words) <= 3:
                k = cats[0]
            elif not words or words[0] != entity:
                k = cats[1] if words and words[0] in same_type[b] else cats[2]
            elif "answer" not in words:
                k = cats[3]
            elif len(parts) < 2 or not target_re.match(parts[1]):
                k = cats[4]
            elif words[-1] == attr(c, t):
                k = cats[6]
            else:
                k = cats[5]
            count[k] += 1
            examples.setdefault(k, (q, out))
    print_table(["结果", "比例"], [[k, f"{100 * count[k] / n:.1f}%"] for k in cats])
    print()
    print_table(["类型", "问题", "回答"], [[k, q, o] for k, (q, o) in examples.items()], aligns=["<", "<", "<"])
    print_observations([
        f"第一步就错的最多：抄错了问题里的实体 {100 * count[cats[1]] / n:.1f}%（问 braltar，写的却是 'vuren is the capital of …'），"
        f"第一句根本不是该写的那句 {100 * count[cats[2]] / n:.1f}%。完整句子的第一步要把问题里的首都或语言**照抄**下来，"
        "迷你模型没有归纳头（第 2 讲 §8.6），抄不准；第一步一错，后面每一步都建立在错的基础上——**错误沿着思维链传播**。",
        f"套路的痕迹（{100 * count[cats[4]] / n:.1f}%）：第一句写对了，第二句却套用了**训练过的**组合（问方位，写的却是领导人那句）。"
        f"另有 {100 * count[cats[0]] / n:.1f}% 干脆没写推理，只写了一个国名。模型把'先写推理'学成了几种组合的固定套路。",
        f"推理写对了、却接着写起了百科（{100 * count[cats[3]] / n:.1f}%，'… the leader of volel is kiro .'）："
        "完整的句子太像预训练里的百科词条，模型'认出'了这种文档，就按百科的习惯写下去——第 1 讲'base 模型续写文档'的又一次出现。",
        "scratch 格式避开了这些错误：它不需要照抄任何东西，也不像任何一种预训练文档。**思维链写成什么样，要适合这个模型**——"
        "对真实的大模型，完整的自然语言推理效果最好；对能力有限的模型，越短、越结构化的中间步骤越可靠。",
    ])


def main():
    tok = None
    models = {}
    for fmt in ("direct", "sentence", "scratch"):
        m, tok, _ = get_cot_model(fmt=fmt)
        models[fmt] = m
    demo1(models, tok)
    demo2(models["sentence"], tok)


if __name__ == "__main__":
    main()
