"""复现 task 2a1bc25d 的「工具意图漏成正文」现象。 用 build_agent(resume=True) 复原真实 system prompt + 全套工具 schema(只读 DB,不跑 loop 不写库),再拿原始用户消息 + 真实 tools 直接打 opus48,看网关回不回结构化 tool_calls。stream / non-stream 两条路径各打一发。输出 ASCII(Windows GBK 安全), 完整正文写文件。 """ import os import sys from pathlib import Path from uuid import UUID ROOT = Path(__file__).resolve().parent.parent env = ROOT / ".env" if env.exists(): for line in env.read_text(encoding="utf-8").splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "True") sys.path.insert(0, str(ROOT)) import litellm # noqa: E402 from core.agent_builder import build_agent # noqa: E402 _tag = (sys.argv[1] if len(sys.argv) > 1 else "default").replace(".", "_").replace("/", "_") OUT = open(ROOT / "scripts" / f"_narrated_probe_{_tag}.txt", "w", encoding="utf-8") def w(*a): print(*a, file=OUT) USER_ID = UUID("5c8af5a4-48cb-4515-bf88-822816e383c9") TASK_ID = "2a1bc25d-d045-4ecd-acc9-b3ee2c8ccf09" USER_MSG = "基于中国建材总院对接景德镇市汇报材料 制作ppt" # 可传档位覆盖(同 task 的 system prompt + 31 工具,只换模型层),默认用 task 自身档。 MODEL_OVERRIDE = sys.argv[1] if len(sys.argv) > 1 else None agent, session, sid, tstate, wd = build_agent( user_id=USER_ID, session_id=TASK_ID, resume=True, model_name=MODEL_OVERRIDE, ) schemas = agent.executor.schemas() caps = agent.caps llm = agent.llm sys_msg = next((m for m in session.messages if m.get("role") == "system"), None) sys_txt = sys_msg["content"] if sys_msg else "" w("[SETUP]") w(f" model_id={caps.model_id} profile={caps.family}.{caps.variant}") w(f" api_base={llm.api_base} temp={caps.optimal_temperature} " f"parallel_tools={caps.parallel_tools}") w(f" system_prompt chars={len(sys_txt)}") w(f" tools count={len(schemas)}") w(f" tool names={[s['function']['name'] for s in schemas]}") w("") messages = [ {"role": "system", "content": sys_txt}, {"role": "user", "content": USER_MSG}, ] kwargs_base = dict( model=caps.model_id, api_base=llm.api_base, api_key=llm.api_key, messages=messages, tools=schemas, temperature=caps.optimal_temperature, timeout=300, ) if caps.parallel_tools: kwargs_base["parallel_tool_calls"] = True stdout_lines = [] def probe(tag, stream): w(f"========== {tag} ==========") try: if stream: chunks = list(litellm.completion( **kwargs_base, stream=True, stream_options={"include_usage": True})) resp = litellm.stream_chunk_builder(chunks) else: resp = litellm.completion(**kwargs_base) except Exception as e: w(f"[FAIL] {type(e).__name__}: {str(e)[:300]}") stdout_lines.append(f"{tag}: FAIL {type(e).__name__}") return msg = resp.choices[0].message tc = msg.tool_calls or [] content = msg.content or "" u = resp.usage w(f" usage prompt={u.prompt_tokens} completion={u.completion_tokens}") w(f" tool_calls count={len(tc)}") for i, t in enumerate(tc): w(f" [{i}] {t.function.name}({(t.function.arguments or '')[:200]})") w(f" content chars={len(content)}") w(" content:") w(content[:2000]) w("") # narration 特征:无结构化 tool_calls 但正文里出现反引号工具名 bullet 参数 narrated = (not tc) and ("`" in content) and ("\n- " in content or "No result received" in content) verdict = ("STRUCTURED tool_calls" if tc else "NARRATED-as-text" if narrated else "plain-text-no-tools") stdout_lines.append(f"{tag}: {verdict} (tc={len(tc)}, content={len(content)}c)") probe("NON-STREAM", stream=False) probe("STREAM", stream=True) OUT.close() for ln in stdout_lines: print(ln) print("full -> scripts/_narrated_probe.txt")