"""Agent 编排:自然语言 → 受控工具调用循环 → 结论(AGENT.md §29 假设-实验-分析循环)。""" from __future__ import annotations import json import re from typing import Any from app.agent.llm import SYSTEM_TEMPLATE, LLMClient from app.agent.tools import Tool, tools_schema from app.agent.tools_impl import build_tools MAX_TOOL_ROUNDS = 5 _DECISION_PATTERN = re.compile(r"\{.*\}", re.DOTALL) def _parse_decision(text: str) -> dict[str, Any]: """容忍 LLM 输出中的代码块/前后缀,提取首个 JSON 对象。""" match = _DECISION_PATTERN.search(text) if not match: raise ValueError(f"无法从模型输出中解析动作 JSON:{text[:200]}") try: return json.loads(match.group(0)) except json.JSONDecodeError as exc: raise ValueError(f"模型输出的 JSON 不合法:{text[:200]}") from exc class AgentService: """受控研究 Agent:每轮让 LLM 决策 tool/final,执行工具并把结果回喂,直至 final。""" def __init__(self, llm: LLMClient, tools: list[Tool] | None = None) -> None: self._llm = llm self._tools = {t.name: t for t in (tools or build_tools())} def _by_name(self, name: str) -> Tool: tool = self._tools.get(name) if tool is None: raise ValueError(f"工具不存在:{name}(可用 {sorted(self._tools)})") return tool def chat(self, message: str, max_rounds: int = MAX_TOOL_ROUNDS) -> dict: messages: list[dict] = [ { "role": "system", "content": SYSTEM_TEMPLATE.format(tools=tools_schema(list(self._tools.values()))), }, {"role": "user", "content": message}, ] actions: list[dict] = [] for _round in range(max_rounds): try: decision = _parse_decision(self._llm.chat(messages)) except ValueError as exc: # LLM 回复格式异常:回传错误要求重试一次结构化输出 messages.append( { "role": "user", "content": f"输出格式错误,请只输出一行 JSON(tool 或 final):{exc}", } ) continue if "final" in decision: return {"reply": str(decision["final"]), "actions": actions} tool_name = str(decision.get("tool", "")) args = decision.get("args") or {} if not isinstance(args, dict): args = {} tool = self._by_name(tool_name) # 白名单之外的调用直接报错 output = tool.invoke(args) actions.append({"tool": tool_name, "args": args, "output": output[:1000]}) messages.append( { "role": "assistant", "content": f'{{"tool": "{tool_name}", "args": {json.dumps(args, ensure_ascii=False)}}}', } ) messages.append( { "role": "user", "content": f"工具 {tool_name} 返回:\n{output}\n请继续(如需再调用输出 tool,否则输出 final)。", } ) # 轮次耗尽:强制收尾 messages.append( { "role": "user", "content": "已达工具调用轮次上限,请直接给出基于已有事实的最终结论(只输出 final JSON)。", } ) try: decision = _parse_decision(self._llm.chat(messages)) except ValueError: decision = { "final": "研究轮次耗尽且模型未给出结构化结论,请人工查看 actions 中的实验输出。" } return {"reply": str(decision.get("final", decision)), "actions": actions}