假设客服 AI agent 发送了不可退款回答,但实际最新政策允许退款。人首先应该检查流程:是否读取了旧政策文档、是否读对了但应用回复规则错误,或是否没有批准步骤就立即发送。但如果没有执行记录,只剩一个错误回答,就很难在组织内解释错误发生在哪里。例如,如果政策解释是对的,但批准步骤被跳过并立即发送,那么问题可能是运行控制失败,而不是模型知识问题。
importasyncioimporthashlibimportjsonimportosfrompathlibimportPathfrompprintimportpprintimporturllib.errorimporturllib.requestfromagentsimportAgent,Runner,function_tool,traceREQUEST="Please tell me whether a refund is available after a service outage."TRACE_WORKFLOW="refund-support-harness"ARTIFACT_DIR=Path(".tmp/p6-15-2-harness-runs")OLLAMA_MODEL=os.environ.get("OLLAMA_MODEL","qwen2.5:1.5b")OLLAMA_URL=os.environ.get("OLLAMA_URL","http://127.0.0.1:11434/api/generate")POLICY_STORE={"2025_12_01":{"document_id":"refund_policy_2025_12_01","refund_allowed_after_outage":False,"text":"Refund is not allowed after a service outage.",},"2026_06_29":{"document_id":"refund_policy_2026_06_29","refund_allowed_after_outage":True,"text":"Refund is allowed after a service outage.",},}defread_policy_document_local(policy_version:str)->dict:policy=POLICY_STORE[policy_version]return{"policy_version":policy_version,**policy}defretrieved_policy_docs(order:str)->list[dict]:versions_by_order={"old_first":["2025_12_01","2026_06_29"],"current_first":["2026_06_29","2025_12_01"],}return[read_policy_document_local(version)forversioninversions_by_order[order]]@function_tooldefread_policy_document(policy_version:str)->dict:"""Return the refund policy document selected by version."""returnread_policy_document_local(policy_version)@function_tool(needs_approval=True)defsend_refund_reply(customer_id:str,answer:str)->str:"""Send a refund reply after human approval."""returnf"queued reply to {customer_id}: {answer}"refund_agent=Agent(name="Refund support agent",instructions=("Answer in English. Read the refund policy document before drafting. ""If the answer will be sent to a customer, use the approval-required tool."),tools=[read_policy_document,send_refund_reply],)definspect_tool_contract(tool):return{"name":tool.name,"required_inputs":tool.params_json_schema.get("required",[]),"needs_approval":bool(tool.needs_approval),}defbuild_model_prompt(request:str,policy_docs:list[dict])->str:policy_lines="\n".join("- {policy_version}: {text}".format(**doc)fordocinpolicy_docs)returnf"""Return only compact JSON with these keys:policy_version, answer_en, send_reply_intent.Use true or false for send_reply_intent.User request:{request}Retrieved policy documents are ordered by search rank:{policy_lines}Use the top-ranked document unless the document itself clearly says it is obsolete.Choose the policy version you used and draft a short English answer."""defcall_local_model(prompt:str)->str:payload={"model":OLLAMA_MODEL,"prompt":prompt,"stream":False,"format":"json","options":{"temperature":0},}request=urllib.request.Request(OLLAMA_URL,data=json.dumps(payload).encode("utf-8"),headers={"Content-Type":"application/json"},)try:withurllib.request.urlopen(request,timeout=90)asresponse:data=json.loads(response.read().decode("utf-8"))except(urllib.error.URLError,TimeoutError)aserror:raiseRuntimeError("Ollama is not reachable. Start Ollama and check `ollama list` "f"for model `{OLLAMA_MODEL}`.")fromerrorreturndata["response"]defparse_model_json(raw_text:str)->dict:cleaned=raw_text.strip()ifcleaned.startswith("`"):cleaned=cleaned.strip("`")cleaned=cleaned.removeprefix("json").strip()returnjson.loads(cleaned)defnormalize_model_decision(raw_text:str)->dict:try:decision=parse_model_json(raw_text)return{"parse_ok":True,"policy_version":decision.get("policy_version"),"answer_en":decision.get("answer_en"),"send_reply_intent":normalize_boolean(decision.get("send_reply_intent")),"raw_text":raw_text,}exceptjson.JSONDecodeErroraserror:return{"parse_ok":False,"policy_version":None,"answer_en":"","send_reply_intent":False,"parse_error":str(error),"raw_text":raw_text,}defnormalize_boolean(value)->bool:ifisinstance(value,bool):returnvalueifisinstance(value,str):returnvalue.strip().lower()in{"true","yes","y","1"}ifisinstance(value,(int,float)):returnvalue==1returnFalsedefinput_hash(request):returnhashlib.sha256(request.encode("utf-8")).hexdigest()[:12]defselect_policy(model_decision:dict,policy_docs:list[dict])->tuple[dict,bool]:selected_version=model_decision["policy_version"]ifselected_versioninPOLICY_STORE:returnread_policy_document_local(selected_version),Falsereturnpolicy_docs[0],Truedefbuild_run_record(agent,request,retrieval_order,run_id):tool_contracts=[inspect_tool_contract(tool)fortoolinagent.tools]policy_docs=retrieved_policy_docs(retrieval_order)prompt=build_model_prompt(request,policy_docs)raw_model_output=call_local_model(prompt)model_decision=normalize_model_decision(raw_model_output)observations=[{"event":"input","value":request},{"event":"retrieved_documents","order":retrieval_order,"value":policy_docs},{"event":"model_prompt","language":"en","value":prompt},{"event":"model_output","model":OLLAMA_MODEL,"value":raw_model_output},{"event":"model_decision","value":model_decision},{"event":"tool_contracts","value":tool_contracts},]policy,unknown_policy_version=select_policy(model_decision,policy_docs)observations.append({"event":"tool_output","tool":"read_policy_document","value":policy})approval_tool=next(toolfortoolintool_contractsiftool["name"]=="send_refund_reply")ifmodel_decision["send_reply_intent"]andapproval_tool["needs_approval"]:gate_status="blocked_for_human_approval"send_status="not_sent"elifmodel_decision["send_reply_intent"]:gate_status="not_required"send_status="sent"else:gate_status="not_requested"send_status="not_sent"observations.append({"event":"approval_gate","tool":"send_refund_reply","status":gate_status,})latest_policy_version="2026_06_29"exception_flags={"model_output_parse_error":notmodel_decision["parse_ok"],"unknown_policy_version":unknown_policy_version,"stale_policy_selected":policy["policy_version"]!=latest_policy_version,"send_intent_blocked_by_gate":gate_status=="blocked_for_human_approval",}artifact_path=ARTIFACT_DIR/f"{run_id}.json"run_report={"agent":agent.name,"model":OLLAMA_MODEL,"answer":model_decision["answer_en"],"retrieval_order":retrieval_order,"policy_version":policy["policy_version"],"document_id":policy["document_id"],"send_status":send_status,"gate_status":gate_status,"exception_flags":exception_flags,"trace":{"workflow":TRACE_WORKFLOW,"group_id":run_id},"observation_count":len(observations),"artifact_path":str(artifact_path),"replay_id":run_id,}return{"schema_version":"p6-15-2-local-run-v1","run_id":run_id,"input_hash":input_hash(request),"observations":observations,"run_report":run_report,}defsave_run_record(record):ARTIFACT_DIR.mkdir(parents=True,exist_ok=True)path=ARTIFACT_DIR/f"{record['run_id']}.json"path.write_text(json.dumps(record,ensure_ascii=False,indent=2),encoding="utf-8")returnpathdefload_run_record(run_id):path=ARTIFACT_DIR/f"{run_id}.json"returnjson.loads(path.read_text(encoding="utf-8"))defrun_and_record(agent,request,retrieval_order,run_id):record=build_run_record(agent,request,retrieval_order,run_id)save_run_record(record)returnrecorddefcompare_saved_runs(before_run_id,after_run_id):before=load_run_record(before_run_id)after=load_run_record(after_run_id)before_report=before["run_report"]after_report=after["run_report"]return{"same_input":before["input_hash"]==after["input_hash"],"changed_retrieval_order":before_report["retrieval_order"]!=after_report["retrieval_order"],"changed_policy_version":before_report["policy_version"]!=after_report["policy_version"],"changed_answer":before_report["answer"]!=after_report["answer"],"gate_kept":before_report["gate_status"]==after_report["gate_status"],"stale_policy_fixed":(before_report["exception_flags"]["stale_policy_selected"]andnotafter_report["exception_flags"]["stale_policy_selected"]),"before":before_report,"after":after_report,}asyncdefrun_live_agent(agent,request,replay_id):withtrace(TRACE_WORKFLOW,group_id=replay_id):result=awaitRunner.run(agent,("Customer ID: C-1042\n"f"User request: {request}\n""Use policy version 2026_06_29."),max_turns=6,)return{"final_output":result.final_output,"replay_id":replay_id,}first_run=run_and_record(refund_agent,REQUEST,"old_first","refund-support-run-001")second_run=run_and_record(refund_agent,REQUEST,"current_first","refund-support-run-002")replayed_first_run=load_run_record("refund-support-run-001")important_events={"model_decision","tool_output","approval_gate"}important_observations=[]foreventinreplayed_first_run["observations"]:ifevent["event"]notinimportant_events:continueifevent["event"]=="model_decision":event={**event,"value":{k:vfork,vinevent["value"].items()ifk!="raw_text"}}important_observations.append(event)print("[first run report]")pprint(first_run["run_report"])print()print("[important observations]")pprint(important_observations)print()print("[replay comparison]")pprint(compare_saved_runs(first_run["run_id"],second_run["run_id"]))ifos.environ.get("RUN_LIVE_AGENT")=="1"andos.environ.get("OPENAI_API_KEY"):print("\n[live sdk run]")pprint(asyncio.run(run_live_agent(refund_agent,REQUEST,"refund-support-live-001")))else:print("\n[live sdk run skipped]")print("Set RUN_LIVE_AGENT=1 and OPENAI_API_KEY to call Runner.run().")
没有 API key 时,结果如下。这个输出包含本地模型实际生成的政策选择和回答草稿。从 harness 视角看,重要的不是回答句子本身,而是选择旧政策的运行和选择当前政策的运行如何被记录并比较。