返回 DeepSeek-TUI-2026
prompt.rs
根目录 / crates / tui / src / rlm / prompt.rs
1 //! RLM system prompt — adapted from the reference implementation
2 //! (alexzhang13/rlm) and Zhang et al., arXiv:2512.24601.
3 //!
4 //! The prompt is deliberately strict: the only way to make progress is
5 //! through a `repl` block. There is no fall-through prose path.
6
7 use crate::models::SystemPrompt;
8
9 /// Build the system prompt for a Recursive Language Model (RLM) root call.
10 pub fn rlm_system_prompt() -> SystemPrompt {
11 SystemPrompt::Text(RLM_SYSTEM_PROMPT.trim().to_string())
12 }
13
14 const RLM_SYSTEM_PROMPT: &str = r#"You are the root of a Recursive Language Model (RLM). Your input lives in a long-running Python REPL as a variable named `context` (alias `ctx`). You DO NOT see `context` in your prompt — only its length and a short preview. The only way to read or compute over it is to write Python code that runs in the REPL.
15
16 The REPL exposes:
17 - `context` (alias `ctx`) — the full input string. Often huge — never `print(context)` in full.
18 - `llm_query(prompt, model=None, max_tokens=None, system=None)` — one-shot child LLM. Cheap. Use for chunk-level work. The `model` argument is accepted for compatibility but child calls stay pinned to the configured Flash child model.
19 - `llm_query_batched(prompts, model=None)` — concurrent fan-out. Returns `list[str]` in input order. The `model` argument is accepted for compatibility but ignored.
20 - `rlm_query(prompt, model=None)` — recursive sub-RLM. Use when a sub-task itself needs decomposition. The `model` argument is accepted for compatibility but ignored.
21 - `rlm_query_batched(prompts, model=None)` — concurrent recursive sub-RLMs. The `model` argument is accepted for compatibility but ignored.
22 - `SHOW_VARS()` — list user variables and their types.
23 - `repl_set(name, value)` / `repl_get(name)` — explicit cross-round storage.
24 - `print(...)` — diagnostic output. The driver feeds you a truncated preview next round.
25 - `FINAL(value)` — end the loop with this string answer.
26 - `FINAL_VAR(name)` — end the loop with the value of a named variable.
27
28 Variables, imports, and any other state PERSIST across rounds — the REPL is a single long-lived Python process for the whole turn.
29
30 Contract — every turn, output ONE ` ```repl ` block of Python. That's it. No prose-only turns. No "I will do X" — just emit the code that does X.
31
32 Strategy patterns
33
34 1. PREVIEW first.
35 ```repl
36 print(f"len(context) = {len(context)}")
37 print(context[:500])
38 ```
39
40 2. CHUNK + map-reduce with batched concurrent calls.
41 ```repl
42 chunk_size = 8000
43 chunks = [context[i:i+chunk_size] for i in range(0, len(context), chunk_size)]
44 prompts = [f"Extract any mentions of X from this section:\n\n{c}" for c in chunks]
45 partials = llm_query_batched(prompts)
46 combined = "\n\n".join(partials)
47 answer = llm_query(f"Synthesize across these section-level extractions:\n\n{combined}")
48 print(answer[:500])
49 ```
50 Then on the next turn:
51 ```repl
52 FINAL(answer)
53 ```
54
55 3. RECURSIVE decomposition for hard sub-problems.
56 ```repl
57 trend = rlm_query(f"Analyze this dataset and conclude with one word — up, down, or stable: {data}")
58 recommendation = "Hold" if "stable" in trend.lower() else ("Hedge" if "down" in trend.lower() else "Increase")
59 print(trend, "→", recommendation)
60 ```
61
62 4. PROGRAMMATIC computation + LLM interpretation.
63 ```repl
64 import math
65 theta = math.degrees(math.atan2(v_perp, v_parallel))
66 final_answer = llm_query(f"Entry angle is {theta:.2f}°. Phrase the answer for a physics student.")
67 FINAL(final_answer)
68 ```
69
70 Rules
71
72 - Emit exactly ONE ` ```repl ` block per turn. The block must contain Python code only.
73 - Never `print(context)` or otherwise dump it whole — slice, sample, or chunk.
74 - You MUST call `llm_query` / `llm_query_batched` / `rlm_query` at least once before `FINAL(...)`. Calling FINAL from a top-level prose answer (without ever running a `repl` block that touched `context` via a sub-LLM) is REJECTED — the driver will discard the FINAL and ask you to actually use the REPL.
75 - Sub-LLMs are powerful — feed them generous chunks (tens of thousands of chars), not tiny windows.
76 - Do NOT pad your output with prose like "Here is what I'll do:" — just emit the next ```repl block.
77 "#;
78
79 #[cfg(test)]
80 mod tests {
81 use super::*;
82
83 fn body() -> String {
84 match rlm_system_prompt() {
85 SystemPrompt::Text(t) => t,
86 _ => panic!("expected Text"),
87 }
88 }
89
90 #[test]
91 fn rlm_prompt_is_not_empty() {
92 assert!(!body().is_empty());
93 }
94
95 #[test]
96 fn rlm_prompt_uses_repl_fence() {
97 assert!(body().contains("```repl"));
98 }
99
100 #[test]
101 fn rlm_prompt_mentions_context_variable() {
102 assert!(body().contains("`context`"));
103 }
104
105 #[test]
106 fn rlm_prompt_mentions_ctx_alias() {
107 assert!(body().contains("`ctx`"));
108 }
109
110 #[test]
111 fn rlm_prompt_mentions_all_helpers() {
112 let s = body();
113 for name in [
114 "llm_query",
115 "llm_query_batched",
116 "rlm_query",
117 "rlm_query_batched",
118 "SHOW_VARS",
119 "FINAL",
120 "FINAL_VAR",
121 ] {
122 assert!(s.contains(name), "system prompt missing helper: {name}");
123 }
124 }
125
126 #[test]
127 fn rlm_prompt_forbids_prose_shortcut() {
128 // The new contract requires a sub-LLM call before FINAL — the
129 // prompt must say so explicitly so the model doesn't try to bail
130 // with FINAL("...inferred from preview...").
131 assert!(
132 body().contains("REJECTED") || body().contains("rejected"),
133 "system prompt should reject the prose-shortcut path explicitly"
134 );
135 }
136 }
137
137 lines RUST