| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- """
- 配载智能体 · 命令行入口
- 把 src/ 里那七个模块串起来,变成一个能用的东西。
- 常用命令::
- python main.py tools # 看看有哪些工具
- python main.py ask "40尺箱写哪个贝号?" # 问一个问题
- python main.py ask "..." --show-steps # 顺便看它思考了几个来回
- python main.py ask "..." --no-llm # 只用检索,不调大模型(免费、看原理)
- python main.py chat # 连续对话
- python main.py eval --offline # 工具自检(免费、秒出)
- python main.py eval --online # 端到端评估(要调大模型)
- python main.py eval --all # 两层都跑
- """
- from __future__ import annotations
- import argparse
- import json
- import sys
- from pathlib import Path
- # 让 `python main.py` 在任意目录下执行都能找到 src 包
- ROOT = Path(__file__).resolve().parent
- sys.path.insert(0, str(ROOT))
- from src import ( # noqa: E402
- BayTool,
- CoordTool,
- KnowledgeTool,
- LLM,
- LLMError,
- Memory,
- ReActAgent,
- Retriever,
- StowageCheckTool,
- ToolRegistry,
- check_tools,
- evaluate_agent,
- )
- from src.evaluate import print_agent_report, print_tool_report # noqa: E402
- KB_FILE = ROOT / "data" / "配载知识库.txt"
- CASE_FILE = ROOT / "data" / "测试用例.json"
- NOTE_FILE = ROOT / "outputs" / "长期笔记.md"
- # ======================================================================
- def build_registry(mode: str = "tfidf") -> ToolRegistry:
- """装好 4 个工具(工具系统)。"""
- print(f"⏳ 正在加载知识库并建索引(模式:{mode})…")
- retriever = Retriever(KB_FILE, mode=mode)
- retriever.index()
- print(f"✅ 知识库已就绪,共 {len(retriever.chunks)} 段资料")
- registry = ToolRegistry()
- registry.register(BayTool())
- registry.register(CoordTool())
- registry.register(StowageCheckTool())
- registry.register(KnowledgeTool(retriever, top_k=3))
- return registry
- def build_agent(args) -> ReActAgent:
- llm = LLM()
- print(f"✅ 已接入大模型:{llm.model}")
- registry = build_registry(args.mode)
- # chat / eval 子命令不一定带了 --show-steps 参数,用 getattr 兜一下默认值
- agent = ReActAgent(
- llm=llm,
- registry=registry,
- memory=Memory(note_path=NOTE_FILE),
- max_steps=getattr(args, "max_steps", 5),
- verbose=getattr(args, "show_steps", False),
- )
- return agent
- # ======================================================================
- # 命令一:列出工具
- # ======================================================================
- def cmd_tools(args) -> None:
- registry = build_registry(args.mode)
- print("\n可用工具:\n")
- for name in registry.names:
- tool = registry.get(name)
- print(f" 🔧 {name}")
- print(f" {tool.description}\n")
- print("想试试?python main.py ask \"贝位 01 和 03 合成几号?\"")
- # ======================================================================
- # 命令二:问一句(--no-llm 时只做检索,看 RAG 的原理)
- # ======================================================================
- def cmd_ask(args) -> None:
- if args.no_llm:
- registry = build_registry(args.mode)
- hits = registry.get("知识库检索").run(args.question)
- print("\n" + "=" * 56)
- print("【检索结果】(这些资料会被贴到问题前面,一起发给大模型)")
- print("=" * 56)
- print(hits)
- print("\n" + "=" * 56)
- print("【实际会发给大模型的提示词长这样】")
- print("=" * 56)
- print("只准根据下面资料回答,资料里没有就说「没有提到」。\n")
- print(f"资料:\n{hits}\n")
- print(f"问题:{args.question}")
- return
- agent = build_agent(args)
- result = agent.run(args.question)
- print("\n" + "=" * 56)
- print("最终答案")
- print("=" * 56)
- print(result["answer"])
- print("\n" + "-" * 56)
- print(f"思考-行动轮数:{len(result['steps'])} 是否正常结束:{result['ok']}")
- # ======================================================================
- # 命令三:连续对话(演示"短期记忆")
- # ======================================================================
- def cmd_chat(args) -> None:
- agent = build_agent(args)
- print("\n进入连续对话模式。它能记住你前面说过的话(短期记忆)。")
- print("输入 exit / quit 退出,输入 :clear 清空记忆。\n")
- while True:
- try:
- question = input("你 > ").strip()
- except (EOFError, KeyboardInterrupt):
- print()
- break
- if not question:
- continue
- if question.lower() in {"exit", "quit", ":q"}:
- break
- if question == ":clear":
- agent.memory.clear()
- print("(已清空短期记忆)")
- continue
- result = agent.run(question)
- print(f"\n助手 > {result['answer']}\n")
- print("-" * 56)
- # ======================================================================
- # 命令四:评估
- # ======================================================================
- def cmd_eval(args) -> None:
- report = {}
- if args.offline or args.all:
- registry = build_registry("tfidf") # 离线自检固定用 tfidf,结果才稳定
- report["工具自检"] = check_tools(registry, CASE_FILE)
- print_tool_report(report["工具自检"])
- if args.online or args.all:
- agent = build_agent(args)
- print("\n开始端到端评估(每一题都要调一次大模型,慢是正常的)…\n")
- report["端到端"] = evaluate_agent(agent.run, CASE_FILE, verbose=True)
- print_agent_report(report["端到端"])
- # 报告做增量合并:只跑了一层时,不要把另一层的历史结果抹掉
- out = ROOT / "outputs" / "评估报告.json"
- out.parent.mkdir(parents=True, exist_ok=True)
- merged = {}
- if out.exists():
- try:
- merged = json.loads(out.read_text(encoding="utf-8"))
- except json.JSONDecodeError:
- merged = {}
- merged.update(report)
- out.write_text(json.dumps(merged, ensure_ascii=False, indent=2), encoding="utf-8")
- print(f"\n📄 报告已保存:{out}(本次覆盖的层:{'、'.join(report)})")
- # ======================================================================
- def main() -> int:
- parser = argparse.ArgumentParser(
- description="配载智能体 —— 集装箱船配载规则问答与坐标计算",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog=__doc__,
- )
- parser.add_argument("--mode", default="tfidf", choices=["tfidf", "vector"],
- help="检索模式:tfidf(默认,秒开)或 vector(语义向量,首次要下模型)")
- parser.add_argument("--max-steps", type=int, default=5, help="ReAct 最多循环几轮")
- sub = parser.add_subparsers(dest="command", required=True)
- sub.add_parser("tools", help="列出所有工具")
- p_ask = sub.add_parser("ask", help="问一个问题")
- p_ask.add_argument("question", help="你要问的话")
- p_ask.add_argument("--show-steps", action="store_true", help="打印每一轮的思考过程")
- p_ask.add_argument("--no-llm", action="store_true", help="只用检索,不调用大模型")
- sub.add_parser("chat", help="连续对话")
- p_eval = sub.add_parser("eval", help="跑评估")
- p_eval.add_argument("--offline", action="store_true", help="只跑工具自检(不花钱)")
- p_eval.add_argument("--online", action="store_true", help="只跑端到端评估")
- p_eval.add_argument("--all", action="store_true", help="两层都跑")
- p_eval.add_argument("--show-steps", action="store_true", help="端到端评估时打印思考过程")
- args = parser.parse_args()
- if args.command == "eval" and not (args.offline or args.online or args.all):
- args.offline = True # 默认跑最便宜的那层
- try:
- {
- "tools": cmd_tools,
- "ask": cmd_ask,
- "chat": cmd_chat,
- "eval": cmd_eval,
- }[args.command](args)
- except LLMError as exc:
- print(f"\n❌ 大模型调用出问题:{exc}")
- print(" 检查一下 .env 里的 LLM_API_KEY / LLM_BASE_URL / LLM_MODEL_ID。")
- return 1
- except FileNotFoundError as exc:
- print(f"\n❌ 找不到文件:{exc}")
- return 1
- return 0
- if __name__ == "__main__":
- raise SystemExit(main())
|