不要再用 test_cases 一次性跑完,而是做一个"会话对象 session",把用户每一轮输入、当前 TCL、上次错误、执行日志都存起来。
现在代码是这种模式:
case1 独立生成
case2 独立生成
case3 独立生成
你要改成:
用户:创建长方体
系统:生成 script_v1,保存到 session.current_script
用户:改成高 60
系统:拿着 script_v1 + 历史需求,生成 script_v2
用户:再加保存名 test2
系统:拿着 script_v2 + 历史需求,生成 script_v3
你现在的 main() 里每轮都会 script = "",并且 test_cases 是固定列表,所以它不是对话式记忆。
核心思路
记忆不要指望 LLM 自己记,你自己在 Python 里保存。
最简单结构:
from dataclasses import dataclass, field
from typing import List, Dict, Optional
@dataclass
class ChatTurn:
role: str
content: str
@dataclass
class AgentSession:
session_id: str
history: List[ChatTurn] = field(default_factory=list)
current_script: str = ""
current_output_name: str = "model"
last_error: str = ""
last_exec_log: str = ""
这个 AgentSession 就是"记忆"。
里面保存:
history 用户和助手摘要
current_script 当前最新版 TCL
current_output_name 当前文件名
last_error 上次校验/执行错误
last_exec_log 上次 hmbatch 日志
改法 1:新增一个对话式生成函数
不要用原来的 generate_tcl(llm, query) 单轮模式,新增这个:
def build_history_text(session: AgentSession, max_turns: int = 8) -> str:
turns = session.history[-max_turns:]
return "\n".join(
f"{t.role}: {t.content}"
for t in turns
)
def generate_tcl_with_memory(llm, session: AgentSession, user_input: str) -> str:
"""
多轮对话生成:
- 记住历史用户需求
- 记住当前最新版 TCL
- 支持用户说"改一下""再加上""把高改成60"
"""
print("🧠 正在根据当前对话生成 RAG 检索关键词...", flush=True)
# 检索词不要只用当前 user_input,否则"改高一点"这种话检索不到
search_query = f"""
当前用户新需求:
{user_input}
历史对话:
{build_history_text(session)}
当前已有 TCL:
{session.current_script}
"""
rag_queries = generate_rag_queries(llm, search_query, max_queries=5)
print("🔎 检索关键词:", flush=True)
for q in rag_queries:
print(" -", q, flush=True)
rag_context = retrieve_for_tcl_generation_multi(rag_queries)
prompt = f"""
你是一个 HyperMesh Tcl 多轮对话脚本生成器。
你需要根据用户的最新要求,在已有脚本基础上修改或重新生成完整 TCL。
【重要规则】
1. 用户可能会说"改一下""加上""不要这个""尺寸改成 xx",这时必须参考当前已有 TCL。
2. 每次都输出完整 TCL 脚本,不要只输出差异。
3. 不要输出解释。
4. 不要 Markdown。
5. 不要 JSON。
6. 不要 tool_call。
7. 不要 Python API。
8. 不要 Python 代码。
9. 如果用户是在修改已有模型,优先保留未被用户要求修改的部分。
10. 如果用户明确重新开始,则忽略当前已有 TCL。
【历史对话】
{build_history_text(session)}
【当前已有 TCL】
{session.current_script if session.current_script else "暂无"}
【上次错误】
{session.last_error if session.last_error else "无"}
【上次执行日志】
{session.last_exec_log if session.last_exec_log else "无"}
【用户最新输入】
{user_input}
【RAG 参考资料】
{rag_context}
【输出要求】
只输出完整 HyperMesh TCL 脚本。
"""
result = llm.invoke([
("system", SYSTEM_PROMPT),
("user", prompt),
])
return clean_tcl(result.content)
这个函数和你原来的 generate_tcl() 最大区别是:
它把 历史对话 + 当前脚本 + 上次错误 + 最新用户输入 一起塞给模型。
改法 2:新增一个带重试的对话处理函数
每次用户输入后,都走这个函数:
def handle_user_turn(
llm,
session: AgentSession,
user_input: str,
auto_run: bool = False,
max_retries: int = 2,
) -> str:
"""
处理用户一轮输入:
1. 记录用户输入
2. 基于历史和当前脚本生成新 TCL
3. 校验
4. 成功则更新 session.current_script
5. 可选执行 hmbatch
"""
session.history.append(ChatTurn("user", user_input))
output_name = extract_output_name(user_input, session.current_output_name)
session.current_output_name = output_name
bad_output = ""
last_error = ""
for attempt in range(1, max_retries + 1):
try:
if attempt == 1:
script = generate_tcl_with_memory(llm, session, user_input)
else:
script = force_rewrite_tcl(
llm=llm,
query=user_input,
bad_output=bad_output,
error=last_error,
)
script = validate_tcl(script, output_name)
# 成功后更新"记忆"
session.current_script = script
session.last_error = ""
session.history.append(ChatTurn(
"assistant",
f"已生成并校验通过 TCL,文件名:{output_name}"
))
if auto_run:
result = run_hypermesh_tcl.invoke({
"script": script,
"output_name": output_name,
})
session.last_exec_log = result
session.history.append(ChatTurn(
"tool",
f"hmbatch 执行结果:{result[:1000]}"
))
return f"✅ 已生成并执行。\n\n当前 TCL:\n{script}\n\n执行日志:\n{result}"
return f"✅ 已生成并校验通过。\n\n当前 TCL:\n{script}"
except Exception as e:
last_error = str(e)
session.last_error = last_error
bad_output = locals().get("script", bad_output)
print(f"⚠️ 第 {attempt} 次失败:{last_error}", flush=True)
session.history.append(ChatTurn(
"assistant",
f"生成失败:{session.last_error}"
))
return f"❌ 生成失败:\n{session.last_error}"
这里重点是:
session.current_script = script
这一步才是真正的"记忆更新"。
改法 3:把 main() 改成真正聊天循环
你现在是:
test_cases = [...]
for query in test_cases:
script = ""
...
这会导致每个 case 断开。
改成:
def chat_main():
llm = init_llm()
session = AgentSession(session_id="local_cli")
print("HyperMesh Tcl Agent 已启动。")
print("输入 exit / quit 退出。")
print("输入 run 执行当前 TCL。")
print("输入 show 查看当前 TCL。")
print("输入 reset 清空对话。")
while True:
user_input = input("\n用户> ").strip()
if not user_input:
continue
if user_input.lower() in {"exit", "quit"}:
print("已退出。")
break
if user_input.lower() == "reset":
session = AgentSession(session_id="local_cli")
print("✅ 已清空当前对话记忆。")
continue
if user_input.lower() == "show":
print("\n当前 TCL:")
print(session.current_script or "暂无")
continue
if user_input.lower() == "run":
if not session.current_script:
print("暂无 TCL 可执行。")
continue
result = run_hypermesh_tcl.invoke({
"script": session.current_script,
"output_name": session.current_output_name,
})
session.last_exec_log = result
print(result)
continue
reply = handle_user_turn(
llm=llm,
session=session,
user_input=user_input,
auto_run=False,
max_retries=2,
)
print("\n助手>")
print(reply)
if __name__ == "__main__":
chat_main()
这样用户就可以连续说:
用户> 创建一个 100 50 30 的长方体,文件名叫 mycube
助手> 生成 v1
用户> 高度改成 60
助手> 基于 v1 改成 v2
用户> 文件名改成 block2
助手> 基于 v2 改保存路径
用户> show
助手> 展示当前 TCL
用户> run
助手> 执行当前 TCL
改法 4:如果是 Web 服务,要按 session_id 存
如果你以后用 Gradio / Streamlit / FastAPI,不能只用一个全局 session,否则不同用户会串。
要这样:
SESSIONS: dict[str, AgentSession] = {}
def get_session(session_id: str) -> AgentSession:
if session_id not in SESSIONS:
SESSIONS[session_id] = AgentSession(session_id=session_id)
return SESSIONS[session_id]
FastAPI 大概这样:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
llm = init_llm()
SESSIONS: dict[str, AgentSession] = {}
class ChatRequest(BaseModel):
session_id: str
message: str
auto_run: bool = False
@app.post("/chat")
def chat(req: ChatRequest):
session = get_session(req.session_id)
reply = handle_user_turn(
llm=llm,
session=session,
user_input=req.message,
auto_run=req.auto_run,
)
return {
"session_id": req.session_id,
"reply": reply,
"current_script": session.current_script,
"output_name": session.current_output_name,
"last_error": session.last_error,
"last_exec_log": session.last_exec_log,
}
这样:
用户 A → session_id=A → A 的历史
用户 B → session_id=B → B 的历史
不会互相串。
很关键:执行失败也要进入记忆
你现在的 run_hypermesh_tcl() 会返回 hmbatch 的返回码、stdout、stderr 和成功失败提示,但它不抛异常,只返回字符串。
所以如果你想让模型根据 hmbatch 错误继续修正,要加一个判断:
def run_and_remember(session: AgentSession) -> str:
result = run_hypermesh_tcl.invoke({
"script": session.current_script,
"output_name": session.current_output_name,
})
session.last_exec_log = result
session.history.append(ChatTurn("tool", result[:2000]))
if "❌" in result or "返回码: 0" not in result:
session.last_error = result
return "❌ 执行失败,错误日志已保存到对话记忆。你可以继续说:根据错误修正。"
session.last_error = ""
return "✅ 执行成功。"
然后用户可以说:
根据刚才报错修正
模型就能看到:
【上次执行日志】
...
【上次错误】
...
【当前已有 TCL】
...
这才是完整闭环。
最小可用完整版本
你可以先直接加这几段:
from dataclasses import dataclass, field
from typing import List
@dataclass
class ChatTurn:
role: str
content: str
@dataclass
class AgentSession:
session_id: str
history: List[ChatTurn] = field(default_factory=list)
current_script: str = ""
current_output_name: str = "model"
last_error: str = ""
last_exec_log: str = ""
def build_history_text(session: AgentSession, max_turns: int = 8) -> str:
turns = session.history[-max_turns:]
return "\n".join(f"{t.role}: {t.content}" for t in turns)
def generate_tcl_with_memory(llm, session: AgentSession, user_input: str) -> str:
search_query = f"""
用户最新输入:
{user_input}
历史对话:
{build_history_text(session)}
当前 TCL:
{session.current_script}
"""
rag_queries = generate_rag_queries(llm, search_query, max_queries=5)
rag_context = retrieve_for_tcl_generation_multi(rag_queries)
prompt = f"""
你是 HyperMesh Tcl 多轮对话脚本生成器。
【任务】
根据用户最新输入,结合历史对话和当前已有 TCL,输出新的完整 TCL。
【历史对话】
{build_history_text(session)}
【当前已有 TCL】
{session.current_script if session.current_script else "暂无"}
【上次错误】
{session.last_error if session.last_error else "无"}
【上次执行日志】
{session.last_exec_log if session.last_exec_log else "无"}
【用户最新输入】
{user_input}
【RAG 参考资料】
{rag_context}
【要求】
1. 只输出完整 TCL。
2. 不要解释。
3. 不要 Markdown。
4. 不要 JSON。
5. 不要 tool_call。
6. 不要 Python API。
7. 如果用户只是修改局部要求,保留当前 TCL 中未被要求修改的部分。
8. 如果用户明确说重新开始,则忽略当前 TCL。
"""
result = llm.invoke([
("system", SYSTEM_PROMPT),
("user", prompt),
])
return clean_tcl(result.content)
def handle_user_turn(llm, session: AgentSession, user_input: str) -> str:
session.history.append(ChatTurn("user", user_input))
output_name = extract_output_name(user_input, session.current_output_name)
session.current_output_name = output_name
try:
script = generate_tcl_with_memory(llm, session, user_input)
script = validate_tcl(script, output_name)
session.current_script = script
session.last_error = ""
session.history.append(ChatTurn(
"assistant",
f"已生成 TCL,文件名 {output_name}"
))
return script
except Exception as e:
session.last_error = str(e)
session.history.append(ChatTurn(
"assistant",
f"生成失败:{session.last_error}"
))
return f"❌ 生成失败:\n{session.last_error}"
def chat_main():
llm = init_llm()
session = AgentSession(session_id="local_cli")
print("HyperMesh Tcl 多轮 Agent 已启动。")
print("exit 退出,show 查看当前 TCL,run 执行当前 TCL,reset 清空。")
while True:
user_input = input("\n用户> ").strip()
if user_input.lower() in {"exit", "quit"}:
break
if user_input.lower() == "reset":
session = AgentSession(session_id="local_cli")
print("✅ 已清空。")
continue
if user_input.lower() == "show":
print(session.current_script or "暂无 TCL")
continue
if user_input.lower() == "run":
if not session.current_script:
print("暂无 TCL 可执行。")
continue
result = run_hypermesh_tcl.invoke({
"script": session.current_script,
"output_name": session.current_output_name,
})
session.last_exec_log = result
session.history.append(ChatTurn("tool", result[:2000]))
print(result)
continue
reply = handle_user_turn(llm, session, user_input)
print("\n助手生成 TCL:")
print(reply)
if __name__ == "__main__":
chat_main()
最终一句话
你要的"多轮修正带记忆"不是靠 llm = init_llm() 实现,也不是靠 force_rewrite_tcl() 实现。
应该是:
Python 维护 AgentSession
保存 history
保存 current_script
保存 last_error
保存 last_exec_log
每次用户新输入
把这些记忆重新放进 prompt
让 LLM 输出完整新版 TCL
校验通过后更新 current_script
这才是真正可控的对话记忆。