code-for-logic¶
第5章 · Coding Agent 与代码生成 · 配套项目
chapter5/code-for-logic
项目说明¶
实验 5-2:用代码生成工具提升逻辑思考能力¶
《深入理解 AI Agent》配套代码。
本实验评估 Agent 通过约束求解代码来辅助逻辑思考的能力:为同一个 LLM 配备一个预装
python-constraint 的 Code Interpreter,让它把「骑士与无赖」(Knights & Knaves) 逻辑谜题
转化为形式化的约束满足问题(CSP)——识别变量(每个岛民是骑士还是无赖)、定义约束
(“骑士说真话、无赖说假话”),再调用求解器搜索满足所有约束的解。
我们用一组 12 道 K&K 谜题(2~5 人,均带唯一真值解)对比三种模式:
- 纯思考(pure):LLM 只用自然语言链式推理,直接给答案;
- 代码辅助(code):LLM 用
run_python工具写约束模型并调求解器,再据结果作答; - 约束求解(solver):离线基线,直接用
python-constraint求解结构化陈述, 不需要任何 API/网络——它是确定性求解器路径本身,理论上 100% 正确,用来验证 「把谜题翻译成约束程序并求解」这一核心论点(见下方真实运行结果)。
核心思想:为什么代码辅助更强¶
K&K 谜题的关键建模规则只有一条——对每位居民 X 加一条双条件(等价)约束:
即 X == (该陈述的语义真值)。把它交给确定性求解器穷举所有布尔组合,逻辑上不会出错;
而纯思考在多人、含计数(“恰好两个骑士”)或自指(“我和 B 同类”)的谜题上,很容易在心算
真值传播时出错。
文件说明¶
| 文件 | 作用 |
|---|---|
demo.py |
主程序:跑 纯思考/代码辅助/约束求解 的对照实验,打印准确率对比表 |
csp_solver.py |
离线约束求解器:结构化陈述 DSL + python-constraint 求解(供 demo 的 solver 模式与 build_puzzles 校验共用) |
sandbox.py |
极简 Code Interpreter:子进程沙箱执行模型生成的 Python(预装 python-constraint) |
puzzles.json |
12 道谜题的题面 + 结构化陈述 + 唯一真值解(给 LLM 的只有题面) |
build_puzzles.py |
生成/校验谜题:用 python-constraint 求解并断言每题“解唯一”,可导出精选题或随机生成 |
requirements.txt |
依赖(openai + python-constraint) |
env.example |
环境变量样例 |
last_run.json |
每次运行后自动保存的逐题完整记录(含模型生成的代码),便于复盘 |
快速开始¶
1) 离线约束求解基线(不需要 API Key,推荐先跑)¶
python demo.py --mode solver # 用 python-constraint 离线求解全部 12 题
python demo.py --mode solver --min-people 4 # 只跑 >=4 人的难题
这条路径完全离线、确定性,直接演示「谜题→约束程序→求解」的核心论点,准确率 100%。
2) LLM 对照实验(需要 OPENAI_API_KEY 或 OPENROUTER_API_KEY)¶
cp env.example .env # 然后编辑 .env 填入 OPENAI_API_KEY
# 或直接 export OPENAI_API_KEY=sk-...
python demo.py # 默认 both:纯思考 vs 代码辅助,全部 12 题
python demo.py --mode pure # 只跑纯思考基线
python demo.py --limit 4 # 只跑前 4 题(省钱冒烟测试)
python demo.py --max-people 3 # 只跑 <=3 人的谜题(按难度筛选)
python demo.py --model gpt-4o-mini # 指定模型(默认 gpt-4o-mini)
python demo.py --puzzles my.json --output run.json # 换数据集/输出路径
通用 OpenRouter 兜底:未配置 OPENAI_API_KEY 时,只要设置了 OPENROUTER_API_KEY
即自动改走 OpenRouter(gpt-* → openai/*)。默认模型 gpt-4o-mini 是普通 gpt id,可
直连 OpenAI;仅当把 --model 换成 gpt-5.x 这类需组织实名认证的模型、且设置了
OPENROUTER_API_KEY 时,才会优先走 OpenRouter。
完整参数见 python demo.py --help(中文说明)。
3) 生成/扩充谜题数据集¶
python build_puzzles.py # 导出内置 12 道精选题(默认)
python build_puzzles.py --generate 20 --min-people 3 --max-people 5 --seed 7
python build_puzzles.py --generate 20 --output my.json
随机生成器会用 python-constraint 求解每个候选谜题,只保留「解唯一」的题目。
sandbox.py / csp_solver.py 也可单独运行做自测:
python sandbox.py、python csp_solver.py 都会用 python-constraint 求解一道最简谜题。
真实运行结果(一):离线约束求解基线(--mode solver,无需 API)¶
python demo.py --mode solver 的真实输出(12 道精选题,完全离线、确定性):
== 约束求解(solver,离线) ==
[solver] kk01 (2人) ✓ 解数=1 预测={'A': 'knight', 'B': 'knave'}
[solver] kk05 (3人) ✓ 解数=1 预测={'A': 'knave', 'B': 'knave', 'C': 'knight'}
[solver] kk11 (5人) ✓ 解数=1 预测={'A': 'knight', 'B': 'knight', 'C': 'knave', 'D': 'knave', 'E': 'knight'}
...(其余题略)
------------------------------------------------------------
准确率 100.0%
============================================================
约束求解 准确率: 100.0% (12/12)
这条路径把每题的结构化陈述翻译成 python-constraint 约束并穷举求解,12/12 全对——
它直接证明了「谜题→约束程序→求解」的确定性;LLM 只要把谜题正确翻译成同样的约束,
就能拿到同样 100% 的结果(下节)。随机生成的谜题(build_puzzles.py --generate)经
solver 复核同样 100% 解出且与生成时的唯一解一致。
真实运行结果(二):LLM 对照实验(gpt-4o-mini,12 题)¶
准确率对比表
============================================================
题号 人数 纯思考 代码辅助
------------------------------------------------------------
kk01 2 ✓ ✓
kk02 2 ✓ ✓
kk03 2 ✓ ✓
kk04 3 ✓ ✓
kk05 3 ✗ ✓
kk06 3 ✗ ✓
kk07 3 ✗ ✓
kk08 4 ✗ ✓
kk09 4 ✗ ✓
kk10 4 ✓ ✓
kk11 5 ✗ ✓
kk12 5 ✓ ✓
------------------------------------------------------------
准确率 50.0% 100.0%
============================================================
纯思考 准确率: 50.0% (6/12)
代码辅助 准确率: 100.0% (12/12)
提升(代码辅助 - 纯思考): +50.0 个百分点
说明:这里刻意选用能力较弱的
gpt-4o-mini来暴露对照——纯思考只做对了 6/12 (50%),且错误集中在 3 人及以上、含计数/自指的谜题上(kk05~kk09、kk11),正是心算 真值传播最容易出错的题型;而代码辅助把每句话翻译成双条件约束、交给python-constraint穷举求解,12/12 全对,一举把准确率拉满,净提升 +50 个百分点。这正是本实验想 说明的核心:把逻辑外包给确定性求解器,正确性不再依赖模型自己的推理强弱。gpt-4o-mini有一定随机性,多次运行个别题目可能有小幅波动,但“纯思考明显低于代码辅助”的整体格局稳定。模型与脚手架(harness)是此消彼长的关系:模型足够强时,脚手架可以更薄——模型自己 就能算对;模型不够强时,就需要在脚手架里做更多事(如把逻辑交给代码/求解器)来兜住 正确性。本实验刻意用较弱的
gpt-4o-mini,正是为了让这一对照可见——换成gpt-5.6-luna这类强推理模型,纯思考也能全解,代码增益会收敛为 0。换句话说,代码辅助(乃至离线 solver)真正的价值,是把正确性变成确定性、与模型强弱无关:对更弱的模型或更大/更难 的谜题,纯思考会随人数增加而掉分,而“翻译成约束程序 + 求解器穷举”的路径始终稳定给出正确解。
一道谜题的约束建模代码(模型自动生成,kk11,5 人链式+计数)¶
题面:A 说“B 是骑士”;B 说“C 是无赖”;C 说“D 是骑士”;D 说“E 是无赖”; E 说“我们五人当中至少有两个骑士”。
from constraint import Problem
p = Problem()
for name in ['A', 'B', 'C', 'D', 'E']:
p.addVariable(name, [True, False]) # True=骑士(说真话), False=无赖(说假话)
# 每句话都写成「X == (那句话的真值)」的双条件约束
p.addConstraint(lambda a, b: a == (b == True), ['A', 'B']) # A:"B 是骑士"
p.addConstraint(lambda b, c: b == (c == False), ['B', 'C']) # B:"C 是无赖"
p.addConstraint(lambda c, d: c == (d == True), ['C', 'D']) # C:"D 是骑士"
p.addConstraint(lambda d, e: d == (e == False), ['D', 'E']) # D:"E 是无赖"
p.addConstraint(lambda a, b, c, d, e: e == ((a + b + c + d + e) >= 2),
['A', 'B', 'C', 'D', 'E']) # E:"至少两个骑士"
for s in p.getSolutions():
print({k: ('knight' if v else 'knave') for k, v in s.items()})
# 输出: {'A': 'knight', 'B': 'knight', 'C': 'knave', 'D': 'knave', 'E': 'knight'}
求解器直接穷举 2^5=32 种组合,返回满足全部约束的唯一解——这正是纯思考在链式真值 传播中最容易算错的题型。
注意事项¶
- 成本:默认
gpt-4o-mini(刻意选用较弱模型以显现对照,见上文),跑完 12 题两种模式的开销很小;用MODEL/--model可换更便宜或更强的模型。 - API Key:从环境变量或
.env读OPENAI_API_KEY(或OPENROUTER_API_KEY兜底);用MODEL可换模型。 - 沙箱:
sandbox.py用子进程 + 超时执行代码,属教学用极简沙箱;生产环境应换成 容器/gVisor 等更强隔离。 - 谜题可靠性:
build_puzzles.py用python-constraint求解每题(内置精选题或随机生成), 断言“解唯一”后才写出,确保真值解无歧义;想自己加题就改CURATED或用--generate。
源代码¶
build_puzzles.py¶
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生成/校验「骑士与无赖」(Knights and Knaves)谜题,并导出 puzzles.json。
每道谜题的每句话都用 csp_solver.py 里的结构化 DSL 表示(见该文件顶部说明),
既能渲染成中文题面(给 LLM 看),也能直接翻译成 python-constraint 约束来求解。
本脚本用 python-constraint 校验每题「解唯一」后才写出——这确保真值解无歧义,
同时演示了实验 5-2 的核心:把谜题形式化为 CSP 并用求解器离线求解。
约定:骑士(knight)永远说真话,无赖(knave)永远说假话。t[name]=True 表示骑士。
用法:
python build_puzzles.py # 导出内置的 12 道精选谜题(默认)
python build_puzzles.py --generate 20 # 随机生成 20 道解唯一的谜题
python build_puzzles.py --generate 20 --min-people 3 --max-people 5 --seed 7
python build_puzzles.py --output my.json # 指定输出文件
"""
import argparse
import json
import random
from csp_solver import render_nl, solve, solve_labeled
# 每题:id, 名字列表, dict{name: 结构化陈述}。中文题面由结构化陈述自动渲染,
# 但精选题保留手写的更自然的中文(见 STATEMENTS_NL 覆盖)。
CURATED = [
("kk01", ["A", "B"], {
"A": ["is", "B", "knave"],
"B": ["and", ["is", "A", "knave"], ["is", "B", "knave"]]}),
("kk02", ["A", "B"], {
"A": ["same", "A", "B"],
"B": ["diff", "A", "B"]}),
("kk03", ["A", "B"], {
"A": ["count", "knight", ">=", 1],
"B": ["is", "A", "knave"]}),
("kk04", ["A", "B", "C"], {
"A": ["is", "B", "knave"],
"B": ["is", "C", "knave"],
"C": ["and", ["is", "A", "knave"], ["is", "B", "knave"]]}),
("kk05", ["A", "B", "C"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["same", "A", "B"]}),
("kk06", ["A", "B", "C"], {
"A": ["same", "B", "C"],
"B": ["is", "A", "knave"],
"C": ["same", "C", "A"]}),
("kk07", ["A", "B", "C"], {
"A": ["or", ["is", "A", "knave"], ["is", "B", "knight"]],
"B": ["is", "A", "knight"],
"C": ["is", "B", "knave"]}),
("kk08", ["A", "B", "C", "D"], {
"A": ["same", "B", "D"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knight"],
"D": ["diff", "B", "C"]}),
("kk09", ["A", "B", "C", "D"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knight"],
"D": ["diff", "A", "B"]}),
("kk10", ["A", "B", "C", "D"], {
"A": ["count", "knave", ">=", 3],
"B": ["is", "A", "knave"],
"C": ["is", "B", "knight"],
"D": ["is", "C", "knave"]}),
("kk11", ["A", "B", "C", "D", "E"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knight"],
"D": ["is", "E", "knave"],
"E": ["count", "knight", ">=", 2]}),
("kk12", ["A", "B", "C", "D", "E"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knave"],
"D": ["is", "E", "knight"],
"E": ["same", "A", "C"]}),
]
# 精选题的手写中文题面(比自动渲染更自然)。未覆盖的句子回退到 render_nl。
STATEMENTS_NL = {
("kk01", "B"): "我们两人都不是骑士。",
("kk02", "A"): "我和 B 是同一类人(要么都是骑士,要么都是无赖)。",
("kk02", "B"): "我和 A 是不同类人。",
("kk03", "A"): "我们当中至少有一个骑士。",
("kk04", "C"): "A 和 B 都是无赖。",
("kk06", "C"): "我和 A 是同一类人。",
("kk07", "A"): "我是无赖,或者 B 是骑士。",
("kk09", "D"): "A 和 B 不是同一类人。",
("kk10", "A"): "我们四人当中至少有三个无赖。",
("kk11", "E"): "我们五人当中至少有两个骑士。",
("kk12", "E"): "A 和 C 是同一类人。",
}
def build_puzzle(pid, names, structs, nl_overrides=None):
"""求解校验(要求解唯一)并组装成写入 puzzles.json 的一条记录。"""
sols = solve_labeled(names, structs)
if len(sols) != 1:
raise ValueError(f"{pid} 解不唯一: {len(sols)} 个解 -> {sols}")
solution = sols[0]
nl_overrides = nl_overrides or {}
statements = {n: nl_overrides.get(n, render_nl(structs[n])) for n in names}
lines = [f"{n}: 「{statements[n]}」" for n in names]
desc = (
f"这座岛上有 {len(names)} 位居民:{', '.join(names)}。"
"每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。"
"他们各自说了如下的话:\n" + "\n".join(lines)
)
return dict(id=pid, num_people=len(names), names=names,
statements=statements, statements_struct=structs,
description=desc, solution=solution)
# ---------------- 随机生成器 ----------------
def _random_stmt(speaker, names, rng):
"""为 speaker 随机生成一句合法的结构化陈述。"""
others = [n for n in names if n != speaker]
kind = rng.choice(["is", "is", "same", "diff", "count"])
if kind == "is":
return ["is", rng.choice(others), rng.choice(["knight", "knave"])]
if kind == "same":
return ["same", speaker, rng.choice(others)]
if kind == "diff":
return ["diff", speaker, rng.choice(others)]
# count:全体中某角色的人数满足某比较
role = rng.choice(["knight", "knave"])
op = rng.choice([">=", "<=", "=="])
k = rng.randint(1, len(names))
return ["count", role, op, k]
def generate(count, min_people, max_people, seed):
"""随机生成 count 道「解唯一」的谜题(用 python-constraint 过滤)。"""
rng = random.Random(seed)
names_pool = ["A", "B", "C", "D", "E", "F", "G"]
puzzles = []
attempts = 0
while len(puzzles) < count and attempts < count * 2000:
attempts += 1
n = rng.randint(min_people, max_people)
names = names_pool[:n]
structs = {sp: _random_stmt(sp, names, rng) for sp in names}
if len(solve(names, structs)) != 1: # 只保留解唯一的谜题
continue
pid = f"gen{len(puzzles) + 1:03d}"
puzzles.append(build_puzzle(pid, names, structs))
if len(puzzles) < count:
print(f"警告:{attempts} 次尝试只生成了 {len(puzzles)}/{count} 道解唯一的谜题。")
return puzzles
def build_curated():
out = []
for pid, names, structs in CURATED:
nl = {n: STATEMENTS_NL[(pid, n)]
for n in names if (pid, n) in STATEMENTS_NL}
out.append(build_puzzle(pid, names, structs, nl))
return out
def main():
ap = argparse.ArgumentParser(
description="生成/校验骑士与无赖谜题并导出 puzzles.json"
"(用 python-constraint 离线求解,校验每题解唯一)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__)
ap.add_argument("--generate", type=int, metavar="N", default=0,
help="随机生成 N 道解唯一的谜题(默认 0=导出内置 12 道精选题)")
ap.add_argument("--min-people", type=int, default=2,
help="随机生成时每题最少居民数(默认 2)")
ap.add_argument("--max-people", type=int, default=5,
help="随机生成时每题最多居民数(难度上限,默认 5)")
ap.add_argument("--seed", type=int, default=42,
help="随机种子,保证可复现(默认 42)")
ap.add_argument("--output", default="puzzles.json",
help="输出文件路径(默认 puzzles.json)")
args = ap.parse_args()
if args.generate > 0:
print(f"随机生成 {args.generate} 道谜题"
f"({args.min_people}~{args.max_people} 人,seed={args.seed})...")
out = generate(args.generate, args.min_people, args.max_people, args.seed)
else:
out = build_curated()
for p in out:
print(f"{p['id']}: OK 唯一解 = {p['solution']}")
with open(args.output, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"\n已写出 {len(out)} 题到 {args.output}")
if __name__ == "__main__":
main()
csp_solver.py¶
"""
离线约束求解器:把「骑士与无赖」谜题的结构化陈述翻译成约束满足问题(CSP),
用 python-constraint 库求解——这是实验 5-2 想论证的「代码求解」路径的确定性参考实现。
它不依赖任何 LLM / 网络,可完全离线运行,因此既用于 build_puzzles.py 校验谜题
「解唯一」,也用于 demo.py 的 solver 模式给出约束求解基线(理论上 100% 正确)。
【结构化陈述 DSL】每句话用一个 JSON 可序列化的列表表示,节点形式如下
(True=骑士/说真话,False=无赖/说假话):
["is", target, "knight"|"knave"] # target 是骑士 / 无赖
["same", a, b] # a 和 b 是同一类人
["diff", a, b] # a 和 b 是不同类人
["count", "knight"|"knave", op, k] # 全体中该角色的人数 op k, op ∈ {">=","<=","=="}
["and", s1, s2] # 合取
["or", s1, s2] # 析取
["not", s1] # 否定
关键建模规则:对每位说话者 X 加一条【双条件约束】 `t[X] == eval_stmt(X 的话)`——
X 是骑士当且仅当他的话为真。绝不能把话本身当作硬约束。
"""
from constraint import Problem
_OPS = {">=": lambda a, b: a >= b,
"<=": lambda a, b: a <= b,
"==": lambda a, b: a == b}
def eval_stmt(node, t):
"""在赋值 t(name->bool, True=骑士) 下求某句话的语义真值。"""
tag = node[0]
if tag == "is":
_, target, role = node
return t[target] if role == "knight" else (not t[target])
if tag == "same":
return t[node[1]] == t[node[2]]
if tag == "diff":
return t[node[1]] != t[node[2]]
if tag == "count":
_, role, op, k = node
want = (role == "knight")
cnt = sum(1 for v in t.values() if v == want)
return _OPS[op](cnt, k)
if tag == "and":
return eval_stmt(node[1], t) and eval_stmt(node[2], t)
if tag == "or":
return eval_stmt(node[1], t) or eval_stmt(node[2], t)
if tag == "not":
return not eval_stmt(node[1], t)
raise ValueError(f"未知的陈述节点: {node!r}")
def solve(names, structs):
"""用 python-constraint 求解,返回所有满足约束的赋值(dict name->bool)列表。
names : 居民名字列表
structs : dict name -> 该居民陈述的结构化 DSL
"""
problem = Problem()
for n in names:
problem.addVariable(n, [True, False])
# 对每位说话者加一条双条件约束:t[X] == (X 的话为真)
for speaker in names:
stmt = structs[speaker]
def make_constraint(speaker=speaker, stmt=stmt):
def constraint(*values):
t = dict(zip(names, values))
return t[speaker] == eval_stmt(stmt, t)
return constraint
problem.addConstraint(make_constraint(), names)
return problem.getSolutions()
def solve_labeled(names, structs):
"""求解并把布尔解转成 {name: 'knight'/'knave'}。返回解列表(通常唯一)。"""
out = []
for sol in solve(names, structs):
out.append({n: ("knight" if sol[n] else "knave") for n in names})
return out
def render_nl(node):
"""把结构化陈述渲染成中文题面(供随机生成的谜题使用)。"""
tag = node[0]
if tag == "is":
role = "骑士" if node[2] == "knight" else "无赖"
return f"{node[1]} 是{role}。"
if tag == "same":
return f"{node[1]} 和 {node[2]} 是同一类人。"
if tag == "diff":
return f"{node[1]} 和 {node[2]} 是不同类人。"
if tag == "count":
role = "骑士" if node[1] == "knight" else "无赖"
word = {">=": "至少", "<=": "至多", "==": "恰好"}[node[2]]
return f"我们当中{word}有 {node[3]} 个{role}。"
if tag == "and":
return f"{render_nl(node[1])[:-1]},并且 {render_nl(node[2])}"
if tag == "or":
return f"{render_nl(node[1])[:-1]},或者 {render_nl(node[2])}"
if tag == "not":
return f"以下说法不成立:{render_nl(node[1])}"
raise ValueError(f"未知的陈述节点: {node!r}")
if __name__ == "__main__":
# 自测:kk01 —— A 说"B 是无赖",B 说"我们都不是骑士"
names = ["A", "B"]
structs = {
"A": ["is", "B", "knave"],
"B": ["and", ["is", "A", "knave"], ["is", "B", "knave"]],
}
print("求解结果:", solve_labeled(names, structs))
demo.py¶
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
实验 5-2:用代码生成工具提升逻辑思考能力
对比在三种模式下求解「骑士与无赖」(Knights & Knaves) 谜题的准确率:
1) 纯思考(pure) —— LLM 仅靠自然语言链式推理直接给出答案;
2) 代码辅助(code) —— LLM 配备 Code Interpreter(预装 python-constraint),
把谜题形式化为约束满足问题(CSP),调用求解器搜索答案;
3) 约束求解(solver) —— 【离线,无需 API】直接用 python-constraint 求解结构化
陈述,作为确定性基线(理论上 100% 正确)。
结论预期:约束求解把逻辑推理外包给确定性求解器,准确率应达 90%+,
且显著高于纯思考模式(纯思考在多人、含计数/自指的谜题上容易出错)。
用法:
# 离线约束求解基线(不花钱、不联网,演示核心论点):
python demo.py --mode solver
# LLM 对照实验(需要 OPENAI_API_KEY):
export OPENAI_API_KEY=sk-...
python demo.py # 默认 both:跑 纯思考 vs 代码辅助 全部题目
python demo.py --mode pure # 只跑纯思考
python demo.py --limit 4 # 只跑前 4 题(省钱冒烟测试)
python demo.py --max-people 3 # 只跑不超过 3 人的谜题(按难度筛选)
python demo.py --model gpt-4o-mini # 指定模型(默认 gpt-4o-mini)
python demo.py --puzzles my.json # 换一份谜题数据集
"""
import argparse
import json
import os
import re
import sys
from csp_solver import solve_labeled
from sandbox import run_python
# ---- 读取 .env(如果存在)。避免额外依赖,手写一个极简解析器。----
def _load_dotenv(path=".env"):
if not os.path.exists(path):
return
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
_load_dotenv()
MODEL = os.environ.get("MODEL", "gpt-4o-mini")
# --- 通用 OpenRouter 兜底:无直连 key 时自动改走 OpenRouter ---
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def map_model_to_openrouter(model: str) -> str:
"""把直连模型名映射为 OpenRouter 上的 id(非可映射 id 统一兜底到当前廉价旗舰)。"""
if not model or "/" in model:
return model or "openai/gpt-5.6-luna"
m = model.lower()
if m.startswith(("gpt-", "o1", "o3", "o4")):
return "openai/" + model
if m.startswith("claude"):
if "haiku" in m:
return "anthropic/claude-haiku-4.5"
if "sonnet" in m:
return "anthropic/claude-sonnet-4.6"
return "anthropic/claude-opus-4.8"
if m.startswith("gemini"):
return "google/" + model
return "openai/gpt-5.6-luna"
def build_client_and_model():
"""构造 OpenAI 客户端并返回 (client, model)。
- 有 OPENAI_API_KEY:直连(默认模型 gpt-4o-mini 是普通 gpt id,可直连 OpenAI)。
仅当模型是 gpt-5.x 且同时设置了 OPENROUTER_API_KEY 时才优先走 OpenRouter
(直连 gpt-5.x 需组织实名认证)。
- 无 OPENAI_API_KEY 但有 OPENROUTER_API_KEY:整体改走 OpenRouter。
"""
from openai import OpenAI
global MODEL
api_key = os.environ.get("OPENAI_API_KEY")
base_url = os.environ.get("OPENAI_BASE_URL")
orkey = os.environ.get("OPENROUTER_API_KEY")
prefer_or = bool(orkey) and (MODEL or "").lower().startswith("gpt-5")
if prefer_or or (not api_key and orkey):
api_key, base_url, MODEL = orkey, OPENROUTER_BASE_URL, map_model_to_openrouter(MODEL)
kw = {"api_key": api_key, "timeout": 60.0, "max_retries": 3}
if base_url:
kw["base_url"] = base_url
return OpenAI(**kw), MODEL
def _reasoning(model: str) -> bool:
"""推理模型(gpt-5 / o 系列 / *thinking 等)不接受 temperature=0。"""
return any(k in (model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
# run_python 工具的 function calling 定义
TOOLS = [{
"type": "function",
"function": {
"name": "run_python",
"description": (
"在预装了 python-constraint 库的沙箱中执行 Python 代码,返回 stdout/stderr。"
"用它把逻辑谜题建模为约束满足问题并求解。记得用 print() 打印结果。"
),
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "要执行的完整 Python 代码"}
},
"required": ["code"],
},
},
}]
ANSWER_HINT = (
'推理结束后,请在最后单独用一行输出 JSON 形式的最终答案,'
'键为每个居民的名字,值为 "knight" 或 "knave",例如:'
'{"A": "knight", "B": "knave"}'
)
PURE_SYSTEM = (
"你是逻辑推理专家。在「骑士与无赖」谜题中,骑士永远说真话,无赖永远说假话。"
"请仅凭自己的推理,逐步分析每位居民的身份,找出满足所有陈述的唯一解。\n" + ANSWER_HINT
)
CODE_SYSTEM = (
"你是逻辑推理专家,擅长把谜题转化为形式化约束并用代码求解。"
"在「骑士与无赖」谜题中,骑士永远说真话,无赖永远说假话。\n"
"请务必使用 run_python 工具,用 python-constraint 库把谜题建模为约束满足问题(CSP)来求解。\n\n"
"【最关键的建模规则】不要把某人的陈述直接当成事实约束!"
"正确做法是对每位居民 X 加一条【双条件(等价)约束】:\n"
" X 的布尔值 == (X 那句话在语义上为真)\n"
"含义:X 是骑士(True) 当且仅当 他的话为真;X 是无赖(False) 当且仅当 他的话为假。\n"
"这条规则对每一句话都适用,包括计数类('恰好有两个骑士')和自指类('我和 B 同类')的陈述——"
"都要写成 `X == (那句话的真值表达式)`,绝不能把 `(那句话的真值表达式)` 单独当作硬约束。\n\n"
"示例(设 True=骑士):\n"
" from constraint import Problem\n"
" p = Problem()\n"
" for name in ['A','B','C']:\n"
" p.addVariable(name, [True, False])\n"
" # A 说'我们中恰好有一个骑士' -> A == ( (A+B+C)==1 )\n"
" p.addConstraint(lambda a,b,c: a == ((a+b+c)==1), ['A','B','C'])\n"
" # B 说'C 是无赖' -> B == (not C)\n"
" p.addConstraint(lambda b,c: b == (not c), ['B','C'])\n"
" # C 说'我和 A 是同一类人' -> C == (C == A)\n"
" p.addConstraint(lambda a,c: c == (c == a), ['A','C'])\n"
" for s in p.getSolutions():\n"
" print({k:('knight' if v else 'knave') for k,v in s.items()})\n\n"
"步骤:1) 每人一个布尔变量;2) 每句话写成上面的双条件约束;"
"3) 调用 getSolutions() 枚举所有解并 print。\n"
"最终答案必须严格采用求解器打印出的解,不要用自己的直觉去推翻它。"
"若求解器输出为空,说明约束建错了(很可能漏了双条件),请检查并重跑。\n" + ANSWER_HINT
)
def parse_answer(text, names):
"""从模型输出里提取最后一个形如 {name: knight/knave} 的 JSON 答案。"""
norm = {"knight": "knight", "knave": "knave", "骑士": "knight", "无赖": "knave"}
# 找出所有 {...} 片段,从后往前尝试解析
for m in reversed(list(re.finditer(r"\{[^{}]*\}", text))):
try:
obj = json.loads(m.group(0))
except json.JSONDecodeError:
continue
if not isinstance(obj, dict):
continue
got = {}
for n in names:
if n not in obj:
break
v = str(obj[n]).strip().lower()
v = norm.get(v, norm.get(str(obj[n]).strip(), None))
if v is None:
break
got[n] = v
else:
return got
return None
def call_model(client, system, user, use_tools):
"""跑一轮对话(含可能的多次工具调用),返回 (最终文本, 使用的代码列表)。"""
messages = [{"role": "system", "content": system},
{"role": "user", "content": user}]
codes = []
for _ in range(8): # 最多 8 轮,防止无限循环
kwargs = (dict(model=MODEL, messages=messages, temperature=1, max_tokens=8192)
if _reasoning(MODEL)
else dict(model=MODEL, messages=messages, temperature=0))
if use_tools:
kwargs.update(tools=TOOLS, tool_choice="auto")
resp = client.chat.completions.create(**kwargs)
msg = resp.choices[0].message
if use_tools and msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
try:
code = json.loads(tc.function.arguments).get("code", "")
except json.JSONDecodeError:
code = ""
codes.append(code)
result = run_python(code)
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": result})
continue
return msg.content or "", codes
return "", codes
def run_mode(client, puzzles, mode):
"""跑一种 LLM 模式(pure/code),返回逐题记录列表。"""
system = CODE_SYSTEM if mode == "code" else PURE_SYSTEM
records = []
for p in puzzles:
text, codes = call_model(client, system, p["description"], mode == "code")
pred = parse_answer(text, p["names"])
correct = pred == p["solution"]
records.append(dict(id=p["id"], num=p["num_people"], pred=pred,
gold=p["solution"], correct=correct,
codes=codes, text=text))
mark = "✓" if correct else "✗"
print(f" [{mode:6}] {p['id']} ({p['num_people']}人) {mark} "
f"预测={pred}")
return records
def run_solver(puzzles):
"""离线约束求解模式:直接用 python-constraint 求解结构化陈述,无需 LLM/API。"""
records = []
for p in puzzles:
struct = p.get("statements_struct")
if not struct:
sys.exit(f"错误:谜题 {p['id']} 缺少 statements_struct 字段,"
"请用新版 build_puzzles.py 重新生成 puzzles.json。")
sols = solve_labeled(p["names"], struct)
pred = sols[0] if len(sols) == 1 else None
correct = pred == p["solution"]
records.append(dict(id=p["id"], num=p["num_people"], pred=pred,
gold=p["solution"], correct=correct,
codes=[], text="", num_solutions=len(sols)))
mark = "✓" if correct else "✗"
print(f" [solver] {p['id']} ({p['num_people']}人) {mark} "
f"解数={len(sols)} 预测={pred}")
return records
LABELS = {"pure": "纯思考", "code": "代码辅助", "solver": "约束求解"}
def print_table(columns, puzzles):
"""打印多列准确率对比表。columns = [(mode, records), ...],顺序即列顺序。"""
accs = {m: sum(r["correct"] for r in recs) / len(recs) for m, recs in columns}
header = f"{'题号':<8}{'人数':<6}" + "".join(f"{LABELS[m]:<10}" for m, _ in columns)
print("\n" + "=" * 60)
print("准确率对比表")
print("=" * 60)
print(header)
print("-" * 60)
n = len(puzzles)
for i in range(n):
row = f"{puzzles[i]['id']:<8}{puzzles[i]['num_people']:<6}"
for _, recs in columns:
row += f"{('✓' if recs[i]['correct'] else '✗'):<10}"
print(row)
print("-" * 60)
tail = f"{'准确率':<8}{'':<6}" + "".join(
f"{accs[m]*100:>6.1f}% " for m, _ in columns)
print(tail)
print("=" * 60)
for m, recs in columns:
n_ok = sum(r["correct"] for r in recs)
print(f"{LABELS[m]:<6} 准确率: {accs[m]*100:5.1f}% ({n_ok}/{len(recs)})")
# 若同时有 solver/code 与 pure,报告提升幅度
baseline = next((m for m in ("pure",) if m in accs), None)
best = next((m for m in ("solver", "code") if m in accs), None)
if baseline and best and best != baseline:
print(f"提升({LABELS[best]} - {LABELS[baseline]}): "
f"{(accs[best]-accs[baseline])*100:+.1f} 个百分点")
def main():
global MODEL
ap = argparse.ArgumentParser(
description="实验 5-2:对比纯思考 / 代码辅助 / 约束求解 三种模式求解"
"「骑士与无赖」逻辑谜题的准确率",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__)
ap.add_argument("--mode", choices=["both", "pure", "code", "solver"],
default="both",
help="运行模式:both=纯思考+代码辅助(默认);pure=仅纯思考;"
"code=仅代码辅助;solver=离线约束求解基线(无需 API)")
ap.add_argument("--model", default=MODEL,
help=f"LLM 模型名(默认 {MODEL};solver 模式忽略)")
ap.add_argument("--limit", type=int, default=0,
help="只跑前 N 题(0=全部)")
ap.add_argument("--min-people", type=int, default=0,
help="只跑居民数 >= 该值的谜题(按难度筛选,0=不限)")
ap.add_argument("--max-people", type=int, default=0,
help="只跑居民数 <= 该值的谜题(按难度筛选,0=不限)")
ap.add_argument("--puzzles", default="puzzles.json",
help="谜题数据集路径(默认 puzzles.json)")
ap.add_argument("--output", default="last_run.json",
help="逐题完整记录的输出路径(默认 last_run.json)")
args = ap.parse_args()
MODEL = args.model
with open(args.puzzles, encoding="utf-8") as f:
puzzles = json.load(f)
if args.min_people:
puzzles = [p for p in puzzles if p["num_people"] >= args.min_people]
if args.max_people:
puzzles = [p for p in puzzles if p["num_people"] <= args.max_people]
if args.limit:
puzzles = puzzles[:args.limit]
if not puzzles:
sys.exit("错误:筛选后没有任何谜题,请放宽 --min-people/--max-people/--limit。")
# solver 模式完全离线,不需要 API;其余模式需要 OPENAI_API_KEY。
llm_modes = {"both": ["pure", "code"], "pure": ["pure"],
"code": ["code"], "solver": []}[args.mode]
results = {}
if args.mode == "solver":
print(f"离线约束求解基线 题目数:{len(puzzles)}\n")
print("== 约束求解(solver,离线) ==")
results["solver"] = run_solver(puzzles)
else:
if not (os.environ.get("OPENAI_API_KEY") or os.environ.get("OPENROUTER_API_KEY")):
sys.exit("错误:pure/code/both 模式需要 OPENAI_API_KEY(或 OPENROUTER_API_KEY 兜底)"
"环境变量(可写入 .env)。若只想看离线约束求解基线,请用 --mode solver。")
client, MODEL = build_client_and_model() # 延迟导入 openai + OpenRouter 兜底
print(f"模型:{MODEL} 题目数:{len(puzzles)} 模式:{args.mode}\n")
for m in llm_modes:
print(f"== {LABELS[m]}({m}) ==")
results[m] = run_mode(client, puzzles, m)
print()
# ---- 准确率对比表(按 pure -> code -> solver 的固定列序) ----
columns = [(m, results[m]) for m in ["pure", "code", "solver"] if m in results]
print_table(columns, puzzles)
# ---- 展示一题的约束建模代码与求解结果 ----
code_recs = results.get("code")
if code_recs:
sample = next((r for r in code_recs if r["correct"] and r["codes"]), None)
if sample:
print("\n" + "=" * 60)
print(f"示例:{sample['id']} 的约束建模代码(模型生成)")
print("=" * 60)
print(sample["codes"][0])
print("-- 求解 & 最终答案 --")
print(f"预测={sample['pred']} 真值={sample['gold']}")
# 保存完整记录,便于复盘
payload = dict(model=MODEL, mode=args.mode)
for m, recs in results.items():
payload[m] = recs
payload[f"{m}_acc"] = sum(r["correct"] for r in recs) / len(recs)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print(f"\n完整逐题记录已保存到 {args.output}")
if __name__ == "__main__":
main()
sandbox.py¶
"""
极简 Code Interpreter 沙箱:在子进程中执行模型生成的 Python 代码。
- 用独立子进程运行,避免污染主进程、并可强制超时。
- 子进程使用与主程序相同的解释器(sys.executable),因此已预装 python-constraint。
- 捕获 stdout / stderr 一并返回给模型,让它能看到求解结果或报错信息。
"""
import subprocess
import sys
import tempfile
import os
def run_python(code: str, timeout: int = 20) -> str:
"""在子进程沙箱中执行 code,返回合并后的 stdout/stderr 文本。"""
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False,
encoding="utf-8") as f:
f.write(code)
path = f.name
try:
proc = subprocess.run(
[sys.executable, path],
capture_output=True, text=True, timeout=timeout,
)
out = proc.stdout
if proc.stderr.strip():
out += "\n[stderr]\n" + proc.stderr
if not out.strip():
out = "(代码已执行,但没有任何输出。记得用 print() 打印结果。)"
return out.strip()
except subprocess.TimeoutExpired:
return f"[错误] 代码执行超时(超过 {timeout} 秒)。"
finally:
os.unlink(path)
if __name__ == "__main__":
# 自测:用 python-constraint 求解一个最简单的 K&K 谜题
demo = """
from constraint import Problem
p = Problem()
# True=骑士(说真话), False=无赖(说假话)
p.addVariable('A', [True, False])
p.addVariable('B', [True, False])
# A 说"B 是无赖":A 的真值 == (B 是无赖) 即 A == (not B)
p.addConstraint(lambda a, b: a == (not b), ['A', 'B'])
# B 说"我们都不是骑士":B == (not A and not B)
p.addConstraint(lambda a, b: b == ((not a) and (not b)), ['A', 'B'])
for s in p.getSolutions():
print({k: 'knight' if v else 'knave' for k, v in s.items()})
"""
print(run_python(demo))
puzzles.json¶
[
{
"id": "kk01",
"num_people": 2,
"names": [
"A",
"B"
],
"statements": {
"A": "B 是无赖。",
"B": "我们两人都不是骑士。"
},
"statements_struct": {
"A": [
"is",
"B",
"knave"
],
"B": [
"and",
[
"is",
"A",
"knave"
],
[
"is",
"B",
"knave"
]
]
},
"description": "这座岛上有 2 位居民:A, B。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是无赖。」\nB: 「我们两人都不是骑士。」",
"solution": {
"A": "knight",
"B": "knave"
}
},
{
"id": "kk02",
"num_people": 2,
"names": [
"A",
"B"
],
"statements": {
"A": "我和 B 是同一类人(要么都是骑士,要么都是无赖)。",
"B": "我和 A 是不同类人。"
},
"statements_struct": {
"A": [
"same",
"A",
"B"
],
"B": [
"diff",
"A",
"B"
]
},
"description": "这座岛上有 2 位居民:A, B。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我和 B 是同一类人(要么都是骑士,要么都是无赖)。」\nB: 「我和 A 是不同类人。」",
"solution": {
"A": "knave",
"B": "knight"
}
},
{
"id": "kk03",
"num_people": 2,
"names": [
"A",
"B"
],
"statements": {
"A": "我们当中至少有一个骑士。",
"B": "A 是无赖。"
},
"statements_struct": {
"A": [
"count",
"knight",
">=",
1
],
"B": [
"is",
"A",
"knave"
]
},
"description": "这座岛上有 2 位居民:A, B。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我们当中至少有一个骑士。」\nB: 「A 是无赖。」",
"solution": {
"A": "knight",
"B": "knave"
}
},
{
"id": "kk04",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "B 是无赖。",
"B": "C 是无赖。",
"C": "A 和 B 都是无赖。"
},
"statements_struct": {
"A": [
"is",
"B",
"knave"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"and",
[
"is",
"A",
"knave"
],
[
"is",
"B",
"knave"
]
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是无赖。」\nB: 「C 是无赖。」\nC: 「A 和 B 都是无赖。」",
"solution": {
"A": "knave",
"B": "knight",
"C": "knave"
}
},
{
"id": "kk05",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "A 和 B 是同一类人。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"same",
"A",
"B"
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「A 和 B 是同一类人。」",
"solution": {
"A": "knave",
"B": "knave",
"C": "knight"
}
},
{
"id": "kk06",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "B 和 C 是同一类人。",
"B": "A 是无赖。",
"C": "我和 A 是同一类人。"
},
"statements_struct": {
"A": [
"same",
"B",
"C"
],
"B": [
"is",
"A",
"knave"
],
"C": [
"same",
"C",
"A"
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 和 C 是同一类人。」\nB: 「A 是无赖。」\nC: 「我和 A 是同一类人。」",
"solution": {
"A": "knight",
"B": "knave",
"C": "knave"
}
},
{
"id": "kk07",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "我是无赖,或者 B 是骑士。",
"B": "A 是骑士。",
"C": "B 是无赖。"
},
"statements_struct": {
"A": [
"or",
[
"is",
"A",
"knave"
],
[
"is",
"B",
"knight"
]
],
"B": [
"is",
"A",
"knight"
],
"C": [
"is",
"B",
"knave"
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我是无赖,或者 B 是骑士。」\nB: 「A 是骑士。」\nC: 「B 是无赖。」",
"solution": {
"A": "knight",
"B": "knight",
"C": "knave"
}
},
{
"id": "kk08",
"num_people": 4,
"names": [
"A",
"B",
"C",
"D"
],
"statements": {
"A": "B 和 D 是同一类人。",
"B": "C 是无赖。",
"C": "D 是骑士。",
"D": "B 和 C 是不同类人。"
},
"statements_struct": {
"A": [
"same",
"B",
"D"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knight"
],
"D": [
"diff",
"B",
"C"
]
},
"description": "这座岛上有 4 位居民:A, B, C, D。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 和 D 是同一类人。」\nB: 「C 是无赖。」\nC: 「D 是骑士。」\nD: 「B 和 C 是不同类人。」",
"solution": {
"A": "knave",
"B": "knave",
"C": "knight",
"D": "knight"
}
},
{
"id": "kk09",
"num_people": 4,
"names": [
"A",
"B",
"C",
"D"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "D 是骑士。",
"D": "A 和 B 不是同一类人。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knight"
],
"D": [
"diff",
"A",
"B"
]
},
"description": "这座岛上有 4 位居民:A, B, C, D。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「D 是骑士。」\nD: 「A 和 B 不是同一类人。」",
"solution": {
"A": "knight",
"B": "knight",
"C": "knave",
"D": "knave"
}
},
{
"id": "kk10",
"num_people": 4,
"names": [
"A",
"B",
"C",
"D"
],
"statements": {
"A": "我们四人当中至少有三个无赖。",
"B": "A 是无赖。",
"C": "B 是骑士。",
"D": "C 是无赖。"
},
"statements_struct": {
"A": [
"count",
"knave",
">=",
3
],
"B": [
"is",
"A",
"knave"
],
"C": [
"is",
"B",
"knight"
],
"D": [
"is",
"C",
"knave"
]
},
"description": "这座岛上有 4 位居民:A, B, C, D。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我们四人当中至少有三个无赖。」\nB: 「A 是无赖。」\nC: 「B 是骑士。」\nD: 「C 是无赖。」",
"solution": {
"A": "knave",
"B": "knight",
"C": "knight",
"D": "knave"
}
},
{
"id": "kk11",
"num_people": 5,
"names": [
"A",
"B",
"C",
"D",
"E"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "D 是骑士。",
"D": "E 是无赖。",
"E": "我们五人当中至少有两个骑士。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knight"
],
"D": [
"is",
"E",
"knave"
],
"E": [
"count",
"knight",
">=",
2
]
},
"description": "这座岛上有 5 位居民:A, B, C, D, E。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「D 是骑士。」\nD: 「E 是无赖。」\nE: 「我们五人当中至少有两个骑士。」",
"solution": {
"A": "knight",
"B": "knight",
"C": "knave",
"D": "knave",
"E": "knight"
}
},
{
"id": "kk12",
"num_people": 5,
"names": [
"A",
"B",
"C",
"D",
"E"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "D 是无赖。",
"D": "E 是骑士。",
"E": "A 和 C 是同一类人。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knave"
],
"D": [
"is",
"E",
"knight"
],
"E": [
"same",
"A",
"C"
]
},
"description": "这座岛上有 5 位居民:A, B, C, D, E。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「D 是无赖。」\nD: 「E 是骑士。」\nE: 「A 和 C 是同一类人。」",
"solution": {
"A": "knave",
"B": "knave",
"C": "knight",
"D": "knave",
"E": "knave"
}
}
]