nl2agent.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. """
  2. nl2agent.py — 一句话搭建智能体
  3. Natural Language → YAML Config → Lambda Term → Execute
  4. 用法:
  5. python nl2agent.py "帮我搭一个深度调研助手,能搜索、写代码、做计算,最多20步,带记忆"
  6. python nl2agent.py --interactive # 交互模式
  7. """
  8. from __future__ import annotations
  9. import os
  10. import sys
  11. import json
  12. import yaml
  13. import time
  14. import tempfile
  15. from typing import Optional
  16. # ============================================================
  17. # 1. NL → YAML: 用 LLM 将自然语言描述编译为 agent 配置
  18. # ============================================================
  19. SYSTEM_PROMPT = '''你是一个 Agent 配置编译器。用户用自然语言描述需要什么样的智能体,你输出对应的 YAML 配置。
  20. 你必须严格输出合法 YAML,不要输出任何其他内容(无解释、无 markdown 代码块标记)。
  21. 可用的 agent type:
  22. - simple: 单步对话 agent
  23. - react: 多步推理 agent (带工具调用, Y 组合子循环)
  24. - chain: 流水线 agent (多步顺序执行)
  25. - router: 路由 agent (根据输入分类到不同子 agent)
  26. - parallel: 并行 agent (多个 agent 同时执行后合并)
  27. 可用的 MCP 工具 (onlineTool 下 example-mcp-server 服务器):
  28. - everything_get_sum: 搜索工具,可搜索互联网信息
  29. - chat_improve_prompt: 优化 prompt 的工具
  30. 本地工具 (localTools):
  31. - terminate: 结束任务(react 类型必须包含)
  32. 模型选择:
  33. - dashscope 的 qwen3-max-2026-01-23(默认)
  34. - anthropic 的 claude-sonnet-4-20250514
  35. 配置结构参考:
  36. ```yaml
  37. agentId: string
  38. name: string
  39. description: string
  40. type: react|chain|router|parallel|simple
  41. systemPrompt: |
  42. 系统提示词...
  43. model:
  44. provider: dashscope
  45. name: qwen3-max-2026-01-23
  46. temperature: 0.7
  47. maxTokens: 4096
  48. react: # type=react 时
  49. maxSteps: 10
  50. observationEnabled: true
  51. toolTimeout: 30
  52. chain: # type=chain 时
  53. steps:
  54. - name: step_name
  55. prompt: "步骤提示词"
  56. router: # type=router 时
  57. classifier:
  58. prompt: "分类提示词"
  59. categories: [cat1, cat2]
  60. routes:
  61. cat1: {type: simple, systemPrompt: "..."}
  62. cat2: {type: simple, systemPrompt: "..."}
  63. default: {type: simple, systemPrompt: "..."}
  64. parallel: # type=parallel 时
  65. agents:
  66. - {name: agent1, systemPrompt: "..."}
  67. - {name: agent2, systemPrompt: "..."}
  68. merge: custom
  69. mergePrompt: "合并提示词"
  70. memory:
  71. enabled: true
  72. strategy: local
  73. size: 20
  74. ttl: 7200
  75. guard:
  76. validator: "len(x) > 100"
  77. retry: 1
  78. mcp:
  79. onlineTool:
  80. example-mcp-server:
  81. - everything_get_sum
  82. localTools:
  83. - terminate
  84. policy:
  85. mode: auto
  86. app:
  87. mcp:
  88. custom:
  89. nodes:
  90. example-mcp-server:
  91. url: https://your-mcp-endpoint.example.com
  92. endpoint: /mcp/airouting
  93. headers:
  94. Authorization: "${MCP_AUTH_TOKEN}"
  95. ```
  96. 规则:
  97. 1. react 类型必须在 localTools 中包含 terminate
  98. 2. systemPrompt 要详细、专业、有针对性
  99. 3. 根据用户描述的复杂度选择合适的 type
  100. 4. 如果用户提到"搜索"、"查资料",用 everything_get_sum 工具
  101. 5. 如果用户提到多个独立视角/角度,考虑用 parallel
  102. 6. 如果用户提到步骤/流程,考虑用 chain
  103. 7. 如果用户提到分类/路由/不同情况,考虑用 router
  104. 8. app.mcp.custom.nodes 的配置固定不变(如上面的参考)
  105. 9. 只输出 YAML,不要任何其他文字'''
  106. def nl_to_yaml(description: str, model: str = "qwen3-max-2026-01-23") -> str:
  107. """
  108. 用 LLM 将自然语言描述转为 YAML 配置。
  109. Lambda 语义: nl_to_yaml = λ(description). LLM_{compiler}(description)
  110. """
  111. provider = _detect_provider(model)
  112. if provider == "dashscope":
  113. return _call_dashscope(model, SYSTEM_PROMPT, description)
  114. elif provider == "anthropic":
  115. return _call_anthropic(model, SYSTEM_PROMPT, description)
  116. else:
  117. return _call_dashscope("qwen3-max-2026-01-23", SYSTEM_PROMPT, description)
  118. def _detect_provider(model: str) -> str:
  119. if "qwen" in model.lower() or "dashscope" in model.lower():
  120. return "dashscope"
  121. elif "claude" in model.lower() or "anthropic" in model.lower():
  122. return "anthropic"
  123. return "dashscope"
  124. def _call_dashscope(model, system, user) -> str:
  125. import urllib.request
  126. api_key = os.environ.get("DASHSCOPE_API_KEY", "")
  127. if not api_key:
  128. raise RuntimeError("请设置 DASHSCOPE_API_KEY 环境变量")
  129. url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
  130. body = json.dumps({
  131. "model": model,
  132. "messages": [
  133. {"role": "system", "content": system},
  134. {"role": "user", "content": user},
  135. ],
  136. "temperature": 0.3,
  137. "max_tokens": 4096,
  138. }).encode("utf-8")
  139. req = urllib.request.Request(url, data=body, headers={
  140. "Content-Type": "application/json",
  141. "Authorization": f"Bearer {api_key}",
  142. })
  143. with urllib.request.urlopen(req, timeout=60) as resp:
  144. data = json.loads(resp.read().decode("utf-8"))
  145. return data["choices"][0]["message"]["content"].strip()
  146. def _call_anthropic(model, system, user) -> str:
  147. import anthropic
  148. client = anthropic.Anthropic()
  149. resp = client.messages.create(
  150. model=model, max_tokens=4096, temperature=0.3,
  151. system=system,
  152. messages=[{"role": "user", "content": user}],
  153. )
  154. return resp.content[0].text.strip()
  155. # ============================================================
  156. # 2. YAML → Lambda Term → Execute
  157. # ============================================================
  158. def build_and_run(yaml_str: str, user_input: str, verbose: bool = True) -> str:
  159. """
  160. 从 YAML 字符串编译 Agent 并执行。
  161. 完整流程:
  162. YAML string → parse → Term → term(input) → result
  163. | | |
  164. 解析 编译 β-规约
  165. """
  166. from lambdagent.fromconfig import from_config, lint_config, format_lint
  167. from lambdagent.fromconfig import to_lambda_expr, describe_config
  168. # 写入临时文件
  169. with tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False, encoding='utf-8') as f:
  170. f.write(yaml_str)
  171. tmp_path = f.name
  172. try:
  173. # Lint
  174. if verbose:
  175. results = lint_config(tmp_path)
  176. print(format_lint(results, "generated-agent.yml"))
  177. print()
  178. # Lambda 结构
  179. if verbose:
  180. print(describe_config(tmp_path))
  181. print()
  182. print("Lambda expression:")
  183. print(to_lambda_expr(tmp_path))
  184. print()
  185. # 编译
  186. if verbose:
  187. print("=" * 60)
  188. print(" Compiling YAML → Lambda Term...")
  189. agent = from_config(tmp_path)
  190. if verbose:
  191. print(f" Compiled: {agent}")
  192. print("=" * 60)
  193. print()
  194. # 执行
  195. if verbose:
  196. print(f" Executing: agent(\"{user_input[:50]}...\")")
  197. print(" " + "-" * 56)
  198. t0 = time.time()
  199. result = agent(user_input)
  200. duration = time.time() - t0
  201. if verbose:
  202. print(f"\n Done in {duration:.1f}s")
  203. print("=" * 60)
  204. return result
  205. finally:
  206. os.unlink(tmp_path)
  207. # ============================================================
  208. # 3. 完整流程: 一句话 → Agent
  209. # ============================================================
  210. def one_sentence_to_agent(description: str, user_input: Optional[str] = None,
  211. model: str = "qwen3-max-2026-01-23", verbose: bool = True):
  212. """
  213. 一句话搭建并运行 Agent。
  214. Lambda 语义:
  215. one_sentence_to_agent = λ(desc, input).
  216. let yaml = LLM_{compiler}(desc) in ← 意图解析
  217. let term = from_config(yaml) in ← 编译
  218. term(input) ← β-规约
  219. """
  220. print("=" * 60)
  221. print(" lambdagent — 一句话搭建智能体")
  222. print("=" * 60)
  223. print()
  224. print(f" 描述: \"{description}\"")
  225. print()
  226. # Step 1: NL → YAML
  227. print(" [Step 1] 解析意图,生成配置...")
  228. t0 = time.time()
  229. yaml_str = nl_to_yaml(description, model)
  230. # 清理: 去掉可能的 markdown 代码块标记
  231. yaml_str = yaml_str.strip()
  232. if yaml_str.startswith("```"):
  233. lines = yaml_str.split("\n")
  234. # Remove first and last lines if they are code fences
  235. if lines[0].startswith("```"):
  236. lines = lines[1:]
  237. if lines and lines[-1].strip() == "```":
  238. lines = lines[:-1]
  239. yaml_str = "\n".join(lines)
  240. gen_time = time.time() - t0
  241. print(f" Done ({gen_time:.1f}s)")
  242. print()
  243. # 显示生成的 YAML
  244. print(" [Generated YAML Config]")
  245. print(" " + "-" * 56)
  246. for line in yaml_str.split("\n"):
  247. print(f" {line}")
  248. print(" " + "-" * 56)
  249. print()
  250. # 验证 YAML 合法性
  251. try:
  252. cfg = yaml.safe_load(yaml_str)
  253. if not isinstance(cfg, dict):
  254. print(" [ERROR] Generated YAML is not a valid config dict")
  255. return yaml_str
  256. except yaml.YAMLError as e:
  257. print(f" [ERROR] Invalid YAML: {e}")
  258. return yaml_str
  259. # Step 2: 保存 YAML
  260. config_path = "generated_agent.yml"
  261. with open(config_path, "w", encoding="utf-8") as f:
  262. f.write(yaml_str)
  263. print(f" Config saved to: {config_path}")
  264. print()
  265. # Step 3: Lint + Lambda 结构
  266. from lambdagent.fromconfig import lint_config, format_lint, describe_config, to_lambda_expr
  267. results = lint_config(cfg)
  268. print(format_lint(results, "generated_agent.yml"))
  269. print()
  270. print(describe_config(cfg))
  271. print()
  272. print(" [Lambda Expression]")
  273. print(f" {to_lambda_expr(cfg)}")
  274. print()
  275. # Step 4: 编译
  276. from lambdagent.fromconfig import from_config
  277. print(" [Step 2] Compiling YAML → Lambda Term...")
  278. agent = from_config(config_path)
  279. print(f" Compiled: {agent}")
  280. print()
  281. # Step 5: 如果有输入就执行
  282. if user_input:
  283. print(f" [Step 3] Executing agent(\"{user_input[:60]}\")")
  284. print(" " + "-" * 56)
  285. t0 = time.time()
  286. result = agent(user_input)
  287. duration = time.time() - t0
  288. print()
  289. print(f" [Result] ({duration:.1f}s)")
  290. print(" " + "-" * 56)
  291. print(result)
  292. print(" " + "-" * 56)
  293. return result
  294. else:
  295. print(" Agent compiled successfully! Use agent(input) to execute.")
  296. print()
  297. print(" Example:")
  298. print(f' agent("{description[:30]}相关的问题")')
  299. return agent
  300. # ============================================================
  301. # 4. 交互模式
  302. # ============================================================
  303. def interactive_mode(model: str = "qwen3-max-2026-01-23"):
  304. """交互式一句话搭建 Agent。"""
  305. print("=" * 60)
  306. print(" lambdagent 交互模式")
  307. print(" 输入自然语言描述来搭建智能体")
  308. print(" 输入 :quit 退出")
  309. print("=" * 60)
  310. print()
  311. while True:
  312. try:
  313. desc = input("描述你需要的 Agent:\n> ").strip()
  314. except (EOFError, KeyboardInterrupt):
  315. print("\nBye!")
  316. break
  317. if not desc or desc == ":quit":
  318. print("Bye!")
  319. break
  320. # 询问是否有具体任务
  321. try:
  322. task = input("要执行什么任务? (直接回车跳过):\n> ").strip()
  323. except (EOFError, KeyboardInterrupt):
  324. print("\nBye!")
  325. break
  326. print()
  327. one_sentence_to_agent(desc, task or None, model)
  328. print()
  329. # ============================================================
  330. # 5. 预置 Demo
  331. # ============================================================
  332. DEMO_DESCRIPTIONS = {
  333. "research": (
  334. "帮我搭一个深度调研助手,能用搜索工具查资料,能分析整理信息,"
  335. "如果信息不够就继续搜索,最多研究20步,带记忆功能,"
  336. "最后输出一份完整的、超过500字的调研报告",
  337. "请调研:大语言模型在软件工程领域的最新应用进展,包括代码生成、代码审查、自动化测试等方面"
  338. ),
  339. "customer_service": (
  340. "搭一个智能客服系统,能自动识别用户是咨询技术问题、账号问题还是投诉建议,"
  341. "分别路由给不同的专家处理,技术专家能调用搜索工具查文档",
  342. "我的账号登录不了了,显示密码错误,但我确认密码是对的"
  343. ),
  344. "code_review": (
  345. "做一个代码审查流水线,第一步检查安全漏洞,第二步检查代码风格和可读性,"
  346. "第三步检查性能问题,第四步生成综合审查报告,每一步的输出要超过100字",
  347. "def login(user, pwd):\n q = f\"SELECT * FROM users WHERE name='{user}' AND pass='{pwd}'\"\n return db.execute(q)"
  348. ),
  349. "translator": (
  350. "做一个多语言翻译系统,把输入同时翻译成英文、日文、韩文三个版本并行执行,"
  351. "然后由一个总结专家对比三个翻译版本的质量并给出最终推荐",
  352. "人工智能正在深刻改变软件开发的方式,从代码自动生成到智能调试,开发者的工作效率得到了显著提升。"
  353. ),
  354. }
  355. def run_demo(name: str = "research", model: str = "qwen3-max-2026-01-23"):
  356. """运行预置 Demo。"""
  357. if name not in DEMO_DESCRIPTIONS:
  358. print(f"Available demos: {list(DEMO_DESCRIPTIONS.keys())}")
  359. return
  360. desc, task = DEMO_DESCRIPTIONS[name]
  361. return one_sentence_to_agent(desc, task, model)
  362. # ============================================================
  363. # Main
  364. # ============================================================
  365. if __name__ == "__main__":
  366. import argparse
  367. parser = argparse.ArgumentParser(description="一句话搭建智能体")
  368. parser.add_argument("description", nargs="?", default=None,
  369. help="Agent 的自然语言描述")
  370. parser.add_argument("--task", "-t", default=None,
  371. help="要执行的任务")
  372. parser.add_argument("--interactive", "-i", action="store_true",
  373. help="交互模式")
  374. parser.add_argument("--demo", "-d", default=None,
  375. choices=list(DEMO_DESCRIPTIONS.keys()),
  376. help="运行预置 Demo")
  377. parser.add_argument("--model", "-m", default="qwen3-max-2026-01-23",
  378. help="用于生成配置的模型")
  379. parser.add_argument("--generate-only", "-g", action="store_true",
  380. help="只生成 YAML,不执行")
  381. args = parser.parse_args()
  382. if args.interactive:
  383. interactive_mode(args.model)
  384. elif args.demo:
  385. run_demo(args.demo, args.model)
  386. elif args.description:
  387. task = args.task if not args.generate_only else None
  388. one_sentence_to_agent(args.description, task, args.model)
  389. else:
  390. parser.print_help()
  391. print("\n示例:")
  392. print(' python nl2agent.py "搭一个能搜索和分析的调研助手" -t "调研大模型最新进展"')
  393. print(' python nl2agent.py --demo research')
  394. print(' python nl2agent.py --interactive')
  395. print(f'\n预置 Demo: {list(DEMO_DESCRIPTIONS.keys())}')