run_experiment.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. """
  2. 实验:用 LLM + Dataset 构造 Lambda 演算原语(本地模拟版)
  3. 通过 subprocess 调用 claude CLI 逐个执行 LDS 测试。
  4. 每个测试将 dataset (few-shot examples) + 新输入组成 prompt,
  5. 然后让 LLM 输出结果,对比期望值。
  6. 使用方式:
  7. python run_experiment.py
  8. """
  9. import subprocess
  10. import json
  11. import sys
  12. import time
  13. from dataclasses import dataclass, field
  14. @dataclass
  15. class ExperimentResult:
  16. name: str
  17. tests: list[dict] = field(default_factory=list)
  18. def add(self, input_val: str, expected: str, actual: str):
  19. # 宽松匹配:去空格、去引号、大小写
  20. def normalize(s):
  21. return s.strip().strip("'\"").strip().lower()
  22. passed = normalize(actual) == normalize(expected)
  23. self.tests.append({
  24. "input": input_val,
  25. "expected": expected,
  26. "actual": actual,
  27. "passed": passed,
  28. })
  29. @property
  30. def passed_count(self):
  31. return sum(1 for t in self.tests if t["passed"])
  32. @property
  33. def total_count(self):
  34. return len(self.tests)
  35. def summary(self) -> str:
  36. lines = [
  37. f"\n{'='*60}",
  38. f"实验: {self.name}",
  39. f"通过: {self.passed_count}/{self.total_count}",
  40. f"{'='*60}",
  41. ]
  42. for t in self.tests:
  43. status = "✓" if t["passed"] else "✗"
  44. lines.append(
  45. f" {status} input={t['input']!r:40s} "
  46. f"expected={t['expected']!r:12s} actual={t['actual']!r}"
  47. )
  48. return "\n".join(lines)
  49. def call_llm(prompt: str, max_retries: int = 2) -> str:
  50. """调用 claude CLI 执行 LDS prompt"""
  51. for attempt in range(max_retries + 1):
  52. try:
  53. result = subprocess.run(
  54. ["claude", "-p", prompt, "--model", "sonnet"],
  55. capture_output=True,
  56. text=True,
  57. timeout=30,
  58. )
  59. if result.returncode == 0:
  60. return result.stdout.strip()
  61. else:
  62. if attempt < max_retries:
  63. time.sleep(1)
  64. continue
  65. return f"ERROR: {result.stderr.strip()}"
  66. except subprocess.TimeoutExpired:
  67. if attempt < max_retries:
  68. continue
  69. return "ERROR: timeout"
  70. except FileNotFoundError:
  71. return "ERROR: claude CLI not found"
  72. return "ERROR: max retries exceeded"
  73. def build_prompt(description: str, examples: list[tuple[str, str]], test_input: str) -> str:
  74. """构造 LDS prompt: system instruction + dataset + test input"""
  75. lines = [
  76. "You are a precise function executor. Given the examples below, "
  77. "learn the pattern and apply it to the new input. "
  78. "Output ONLY the result, nothing else. No explanation, no quotes, no extra text.",
  79. "",
  80. f"Function: {description}",
  81. "",
  82. "Examples:",
  83. ]
  84. for inp, out in examples:
  85. lines.append(f" Input: {inp}")
  86. lines.append(f" Output: {out}")
  87. lines.append("")
  88. lines.append(f"Input: {test_input}")
  89. lines.append("Output:")
  90. return "\n".join(lines)
  91. # ============================================================
  92. # 实验定义
  93. # ============================================================
  94. def experiment_succ() -> ExperimentResult:
  95. """实验 1: SUCC 后继函数 ≡ λn.λf.λx. f(n f x)"""
  96. result = ExperimentResult("SUCC (后继函数)")
  97. dataset = [
  98. ("0", "1"), ("1", "2"), ("2", "3"),
  99. ("5", "6"), ("9", "10"), ("13", "14"), ("99", "100"),
  100. ]
  101. tests = [3, 4, 7, 10, 15, 42, 127, 255]
  102. for n in tests:
  103. prompt = build_prompt(
  104. "Given a number n, output n+1 (the successor).",
  105. dataset, str(n)
  106. )
  107. actual = call_llm(prompt)
  108. result.add(str(n), str(n + 1), actual)
  109. return result
  110. def experiment_booleans() -> ExperimentResult:
  111. """实验 2: TRUE/FALSE ≡ λa.λb.a / λa.λb.b"""
  112. result = ExperimentResult("TRUE/FALSE (Church 布尔值)")
  113. true_dataset = [
  114. ("A='apple', B='banana'", "apple"),
  115. ("A='red', B='blue'", "red"),
  116. ("A='1', B='0'", "1"),
  117. ("A='yes', B='no'", "yes"),
  118. ("A='cat', B='dog'", "cat"),
  119. ]
  120. false_dataset = [
  121. ("A='apple', B='banana'", "banana"),
  122. ("A='red', B='blue'", "blue"),
  123. ("A='1', B='0'", "0"),
  124. ("A='yes', B='no'", "no"),
  125. ("A='cat', B='dog'", "dog"),
  126. ]
  127. test_pairs = [("sun", "moon"), ("hello", "world"), ("42", "0"), ("alpha", "omega")]
  128. for a, b in test_pairs:
  129. inp = f"A='{a}', B='{b}'"
  130. # TRUE
  131. prompt_t = build_prompt("Given two options A and B, always select A (the first one).", true_dataset, inp)
  132. actual_t = call_llm(prompt_t)
  133. result.add(f"TRUE({a},{b})", a, actual_t)
  134. # FALSE
  135. prompt_f = build_prompt("Given two options A and B, always select B (the second one).", false_dataset, inp)
  136. actual_f = call_llm(prompt_f)
  137. result.add(f"FALSE({a},{b})", b, actual_f)
  138. return result
  139. def experiment_logic() -> ExperimentResult:
  140. """实验 3: AND/OR/NOT"""
  141. result = ExperimentResult("AND/OR/NOT (逻辑运算)")
  142. and_ds = [("TRUE, TRUE", "TRUE"), ("TRUE, FALSE", "FALSE"),
  143. ("FALSE, TRUE", "FALSE"), ("FALSE, FALSE", "FALSE")]
  144. or_ds = [("TRUE, TRUE", "TRUE"), ("TRUE, FALSE", "TRUE"),
  145. ("FALSE, TRUE", "TRUE"), ("FALSE, FALSE", "FALSE")]
  146. not_ds = [("TRUE", "FALSE"), ("FALSE", "TRUE")]
  147. # AND
  148. for inp, exp in and_ds:
  149. prompt = build_prompt("Logical AND of two boolean values.", and_ds, inp)
  150. actual = call_llm(prompt)
  151. result.add(f"AND({inp})", exp, actual)
  152. # OR
  153. for inp, exp in or_ds:
  154. prompt = build_prompt("Logical OR of two boolean values.", or_ds, inp)
  155. actual = call_llm(prompt)
  156. result.add(f"OR({inp})", exp, actual)
  157. # NOT
  158. for inp, exp in not_ds:
  159. prompt = build_prompt("Logical NOT of a boolean value.", not_ds, inp)
  160. actual = call_llm(prompt)
  161. result.add(f"NOT({inp})", exp, actual)
  162. return result
  163. def experiment_if() -> ExperimentResult:
  164. """实验 4: IF-THEN-ELSE ≡ λcond.λthen.λelse. cond then else"""
  165. result = ExperimentResult("IF-THEN-ELSE (条件分支)")
  166. dataset = [
  167. ("condition=TRUE, then='yes', else='no'", "yes"),
  168. ("condition=FALSE, then='yes', else='no'", "no"),
  169. ("condition=TRUE, then='42', else='0'", "42"),
  170. ("condition=FALSE, then='42', else='0'", "0"),
  171. ("condition=TRUE, then='go', else='stop'", "go"),
  172. ("condition=FALSE, then='go', else='stop'", "stop"),
  173. ]
  174. tests = [
  175. ("condition=TRUE, then='accept', else='reject'", "accept"),
  176. ("condition=FALSE, then='accept', else='reject'", "reject"),
  177. ("condition=TRUE, then='100', else='-1'", "100"),
  178. ("condition=FALSE, then='100', else='-1'", "-1"),
  179. ("condition=TRUE, then='open', else='close'", "open"),
  180. ("condition=FALSE, then='open', else='close'", "close"),
  181. ]
  182. for inp, exp in tests:
  183. prompt = build_prompt(
  184. "IF-THEN-ELSE: If condition is TRUE, output the THEN value. If FALSE, output the ELSE value.",
  185. dataset, inp
  186. )
  187. actual = call_llm(prompt)
  188. result.add(inp, exp, actual)
  189. return result
  190. def experiment_pairs() -> ExperimentResult:
  191. """实验 5: PAIR/FST/SND"""
  192. result = ExperimentResult("PAIR/FST/SND (有序对)")
  193. pair_ds = [
  194. ("a=3, b=7", "(3, 7)"),
  195. ("a=hello, b=world", "(hello, world)"),
  196. ("a=0, b=1", "(0, 1)"),
  197. ]
  198. fst_ds = [
  199. ("(3, 7)", "3"), ("(hello, world)", "hello"),
  200. ("(0, 1)", "0"), ("(42, 99)", "42"),
  201. ]
  202. snd_ds = [
  203. ("(3, 7)", "7"), ("(hello, world)", "world"),
  204. ("(0, 1)", "1"), ("(42, 99)", "99"),
  205. ]
  206. # PAIR
  207. for inp, exp in [("a=10, b=20", "(10, 20)"), ("a=foo, b=bar", "(foo, bar)")]:
  208. prompt = build_prompt("Construct a pair from a and b.", pair_ds, inp)
  209. actual = call_llm(prompt)
  210. result.add(f"PAIR({inp})", exp, actual)
  211. # FST
  212. for inp, exp in [("(5, 8)", "5"), ("(alpha, beta)", "alpha"), ("(100, 200)", "100")]:
  213. prompt = build_prompt("Extract the first element of a pair.", fst_ds, inp)
  214. actual = call_llm(prompt)
  215. result.add(f"FST({inp})", exp, actual)
  216. # SND
  217. for inp, exp in [("(5, 8)", "8"), ("(alpha, beta)", "beta"), ("(100, 200)", "200")]:
  218. prompt = build_prompt("Extract the second element of a pair.", snd_ds, inp)
  219. actual = call_llm(prompt)
  220. result.add(f"SND({inp})", exp, actual)
  221. return result
  222. def experiment_composition() -> ExperimentResult:
  223. """实验 6: 函数组合 DOUBLE ∘ SUCC ≡ (g ∘ f)(x) = g(f(x))"""
  224. result = ExperimentResult("DOUBLE ∘ SUCC (函数组合)")
  225. succ_ds = [("0", "1"), ("1", "2"), ("2", "3"), ("5", "6"), ("9", "10")]
  226. double_ds = [("0", "0"), ("1", "2"), ("2", "4"), ("3", "6"), ("5", "10"), ("10", "20")]
  227. for n in [0, 1, 3, 5, 7, 10]:
  228. # Step 1: SUCC(n)
  229. p1 = build_prompt("Given a number n, output n+1.", succ_ds, str(n))
  230. succ_result = call_llm(p1)
  231. # Step 2: DOUBLE(SUCC(n))
  232. p2 = build_prompt("Given a number n, output 2*n.", double_ds, succ_result)
  233. double_result = call_llm(p2)
  234. expected = str(2 * (n + 1))
  235. result.add(f"DOUBLE(SUCC({n}))", expected, double_result)
  236. return result
  237. def experiment_factorial() -> ExperimentResult:
  238. """实验 7: FACTORIAL —— 递归通过 CoT 展开,等价于 Y 组合子"""
  239. result = ExperimentResult("FACTORIAL (递归/Y 组合子)")
  240. dataset = [
  241. ("0", "0! = 1\nResult: 1"),
  242. ("1", "1! = 1 × 0! = 1 × 1 = 1\nResult: 1"),
  243. ("3", "3! = 3 × 2!\n2! = 2 × 1!\n1! = 1 × 0!\n0! = 1\nSo: 1 × 1 × 2 × 3 = 6\nResult: 6"),
  244. ("5", "5! = 5 × 4!\n4! = 4 × 3!\n3! = 3 × 2!\n2! = 2 × 1!\n1! = 1 × 0!\n0! = 1\nSo: 1 × 1 × 2 × 3 × 4 × 5 = 120\nResult: 120"),
  245. ]
  246. import math
  247. for n in [2, 4, 6, 7, 8, 10]:
  248. prompt = build_prompt(
  249. "Compute n! (factorial) step by step. Show recursive expansion. End with 'Result: <number>'.",
  250. dataset, str(n)
  251. )
  252. actual_raw = call_llm(prompt)
  253. # 提取 Result 行
  254. actual = actual_raw
  255. for line in reversed(actual_raw.split("\n")):
  256. if "Result:" in line:
  257. actual = line.split("Result:")[-1].strip()
  258. break
  259. expected = str(math.factorial(n))
  260. result.add(f"{n}!", expected, actual)
  261. return result
  262. def experiment_church_numerals() -> ExperimentResult:
  263. """实验 8: Church 数 c_n ≡ λf.λx. f^n(x)"""
  264. result = ExperimentResult("CHURCH 数 (f^n(x))")
  265. dataset = [
  266. ("n=0, f='add1', x='0'", "0"),
  267. ("n=1, f='add1', x='0'", "1"),
  268. ("n=2, f='add1', x='0'", "2"),
  269. ("n=3, f='add1', x='0'", "3"),
  270. ("n=0, f='double', x='1'", "1"),
  271. ("n=1, f='double', x='1'", "2"),
  272. ("n=2, f='double', x='1'", "4"),
  273. ("n=3, f='double', x='1'", "8"),
  274. ("n=4, f='double', x='1'", "16"),
  275. ]
  276. # add1 tests
  277. for n in [4, 5, 7, 10]:
  278. prompt = build_prompt(
  279. "Apply function f to value x exactly n times. "
  280. "add1 adds 1 each time. double multiplies by 2 each time.",
  281. dataset, f"n={n}, f='add1', x='0'"
  282. )
  283. actual = call_llm(prompt)
  284. result.add(f"add1^{n}(0)", str(n), actual)
  285. # double tests: double^n(1) = 2^n
  286. for n in [5, 6, 7, 8]:
  287. prompt = build_prompt(
  288. "Apply function f to value x exactly n times. "
  289. "add1 adds 1 each time. double multiplies by 2 each time.",
  290. dataset, f"n={n}, f='double', x='1'"
  291. )
  292. actual = call_llm(prompt)
  293. result.add(f"double^{n}(1)", str(2**n), actual)
  294. return result
  295. # ============================================================
  296. # 主程序
  297. # ============================================================
  298. def main():
  299. print("=" * 60)
  300. print("LDS ≡ Lambda Calculus 实验验证")
  301. print("模式: 本地 Claude CLI 模拟")
  302. print("=" * 60)
  303. # 检查 claude CLI
  304. try:
  305. r = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=5)
  306. print(f"Claude CLI: {r.stdout.strip()}")
  307. except Exception as e:
  308. print(f"错误: 无法找到 claude CLI: {e}")
  309. sys.exit(1)
  310. experiments = [
  311. ("1. 后继函数 SUCC", experiment_succ),
  312. ("2. Church 布尔值 TRUE/FALSE", experiment_booleans),
  313. ("3. 逻辑运算 AND/OR/NOT", experiment_logic),
  314. ("4. 条件分支 IF-THEN-ELSE", experiment_if),
  315. ("5. 有序对 PAIR/FST/SND", experiment_pairs),
  316. ("6. 函数组合 DOUBLE∘SUCC", experiment_composition),
  317. ("7. 递归/阶乘 (Y 组合子)", experiment_factorial),
  318. ("8. Church 数 f^n(x)", experiment_church_numerals),
  319. ]
  320. all_results = []
  321. for name, fn in experiments:
  322. print(f"\n>>> 运行实验 {name}...")
  323. r = fn()
  324. all_results.append(r)
  325. print(r.summary())
  326. # 汇总
  327. total = sum(r.total_count for r in all_results)
  328. passed = sum(r.passed_count for r in all_results)
  329. print("\n" + "=" * 60)
  330. print("汇总结果")
  331. print("=" * 60)
  332. for r in all_results:
  333. rate = r.passed_count / r.total_count * 100 if r.total_count else 0
  334. bar = "█" * int(rate / 5) + "░" * (20 - int(rate / 5))
  335. print(f" {r.name:40s} {r.passed_count:2d}/{r.total_count:2d} {bar} {rate:.0f}%")
  336. print(f"\n {'总计':40s} {passed:2d}/{total:2d} {passed/total*100:.1f}%")
  337. print("=" * 60)
  338. # 保存 JSON
  339. output = {
  340. "model": "claude-sonnet (via CLI)",
  341. "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
  342. "experiments": [
  343. {
  344. "name": r.name,
  345. "passed": r.passed_count,
  346. "total": r.total_count,
  347. "tests": r.tests,
  348. }
  349. for r in all_results
  350. ],
  351. "total_passed": passed,
  352. "total_tests": total,
  353. }
  354. with open("experiment_results.json", "w", encoding="utf-8") as f:
  355. json.dump(output, f, ensure_ascii=False, indent=2)
  356. print("\n结果已保存到 experiment_results.json")
  357. if __name__ == "__main__":
  358. main()