Files
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

328 lines
13 KiB
Python

"""
精简版「航空客服」模拟环境(对标 tau-bench 的航空场景,但去掉复杂度)。
包含三部分:
1. TOOLS —— 暴露给 Agent 的工具(含关键的 transfer_to_human)。
2. run_agent —— 一个带工具调用循环的最小 Agent:给定 system prompt 和用户请求,
返回它是否转接人工、以及最终回复。
3. CASES —— 两组评测用例:
- 保留任务集(holdout):正常请求,Agent 应正确处理(不该转的别转,该转的要转)。
- 边界案例集(boundary):政策争议,Agent 应解释政策而非一转了之。
"""
import json
from config import get_client, get_model, get_temperature, record_completion
# ----------------------------------------------------------------------------
# 1. 工具定义(OpenAI function-calling 格式)
# ----------------------------------------------------------------------------
TOOLS = [
{
"type": "function",
"function": {
"name": "lookup_reservation",
"description": "根据订单号查询乘客的订单详情(航班、舱位、票价类型等)。",
"parameters": {
"type": "object",
"properties": {
"confirmation_code": {"type": "string", "description": "订单号"}
},
"required": ["confirmation_code"],
},
},
},
{
"type": "function",
"function": {
"name": "change_flight",
"description": "为乘客办理改签到指定的新航班。",
"parameters": {
"type": "object",
"properties": {
"confirmation_code": {"type": "string"},
"new_flight": {"type": "string", "description": "新航班号或日期"},
},
"required": ["confirmation_code", "new_flight"],
},
},
},
{
"type": "function",
"function": {
"name": "get_refund_policy",
"description": "查询退票/退款政策。传入票价类型(如 经济舱特价票/全价经济舱/商务舱)。",
"parameters": {
"type": "object",
"properties": {
"fare_type": {"type": "string", "description": "票价类型"}
},
"required": ["fare_type"],
},
},
},
{
"type": "function",
"function": {
"name": "get_baggage_policy",
"description": "查询行李额与逾重费政策。传入舱位等级。",
"parameters": {
"type": "object",
"properties": {
"cabin": {"type": "string", "description": "舱位等级,如 经济舱/商务舱"}
},
"required": ["cabin"],
},
},
},
{
"type": "function",
"function": {
"name": "change_seat",
"description": "为乘客办理选座或换座。",
"parameters": {
"type": "object",
"properties": {
"confirmation_code": {"type": "string"},
"seat": {"type": "string", "description": "目标座位号"},
},
"required": ["confirmation_code", "seat"],
},
},
},
{
"type": "function",
"function": {
"name": "transfer_to_human",
"description": "把对话转接给人工客服。调用后 Agent 不再继续处理本次请求。",
"parameters": {
"type": "object",
"properties": {
"reason": {"type": "string", "description": "转接原因"}
},
"required": ["reason"],
},
},
},
]
# ----------------------------------------------------------------------------
# 2. 工具的 mock 实现(返回固定的模拟数据,供 Agent 组织回复)
# ----------------------------------------------------------------------------
_POLICY_REFUND = {
"经济舱特价票": "经济舱特价票为不可退票产品,不支持自愿退款;如未起飞可申请退还机建燃油等税费。",
"全价经济舱": "全价经济舱起飞前可退,收取 5% 退票手续费。",
"商务舱": "商务舱起飞前可全额退票,不收手续费。",
}
def _run_tool(name: str, args: dict) -> str:
"""执行工具,返回给模型的字符串结果。"""
if name == "lookup_reservation":
return json.dumps(
{
"confirmation_code": args.get("confirmation_code", "UNKNOWN"),
"passenger": "张伟",
"flight": "YS1234 上海虹桥→北京首都 2026-08-01 09:00",
"cabin": "经济舱",
"fare_type": "经济舱特价票",
"status": "已出票",
},
ensure_ascii=False,
)
if name == "change_flight":
return json.dumps(
{"result": "success", "new_flight": args.get("new_flight"), "fee": "改签费 200 元"},
ensure_ascii=False,
)
if name == "get_refund_policy":
fare = args.get("fare_type", "经济舱特价票")
text = _POLICY_REFUND.get(fare, _POLICY_REFUND["经济舱特价票"])
return json.dumps({"fare_type": fare, "policy": text}, ensure_ascii=False)
if name == "get_baggage_policy":
cabin = args.get("cabin") or "经济舱"
free = "20kg" if "经济" in cabin else "30kg"
return json.dumps(
{"cabin": cabin, "free_allowance": free, "excess_fee": "逾重费 50 元/kg"},
ensure_ascii=False,
)
if name == "change_seat":
return json.dumps(
{"result": "success", "seat": args.get("seat")}, ensure_ascii=False
)
return json.dumps({"result": "ok"}, ensure_ascii=False)
# ----------------------------------------------------------------------------
# 3. 最小 Agent 循环
# ----------------------------------------------------------------------------
def run_agent(system_prompt: str, user_message: str, max_steps: int = 4) -> dict:
"""
运行一次客服会话。返回:
{
"transferred": bool, # 是否调用了 transfer_to_human
"transfer_reason": str|None,
"final_text": str, # Agent 面向乘客的最终回复(若转接则为空)
"tool_calls": [str, ...], # 依次调用过的工具名
}
"""
client = get_client()
model = get_model()
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
]
tool_calls_log = []
for _ in range(max_steps):
request = dict(
model=model,
messages=messages,
tools=TOOLS,
temperature=get_temperature(),
)
resp = record_completion(client, kind="task_agent", **request)
msg = resp.choices[0].message
if not msg.tool_calls:
# 模型直接给出面向乘客的文字回复 —— 会话结束
return {
"transferred": False,
"transfer_reason": None,
"final_text": msg.content or "",
"tool_calls": tool_calls_log,
}
# 有工具调用,先把 assistant 消息加入历史
messages.append(msg)
transferred_now = False
transfer_reason = None
for tc in msg.tool_calls:
name = tc.function.name
try:
args = json.loads(tc.function.arguments or "{}")
except json.JSONDecodeError:
args = {}
tool_calls_log.append(name)
if name == "transfer_to_human":
transferred_now = True
transfer_reason = args.get("reason", "")
result = json.dumps({"result": "transferred"}, ensure_ascii=False)
else:
result = _run_tool(name, args)
messages.append(
{"role": "tool", "tool_call_id": tc.id, "content": result}
)
if transferred_now:
return {
"transferred": True,
"transfer_reason": transfer_reason,
"final_text": "",
"tool_calls": tool_calls_log,
}
# 超出步数上限仍未收敛,兜底再要一次纯文本回复
resp = record_completion(
client, kind="task_agent_forced_final",
model=model, messages=messages, temperature=get_temperature(),
)
return {
"transferred": False,
"transfer_reason": None,
"final_text": resp.choices[0].message.content or "",
"tool_calls": tool_calls_log,
}
# ----------------------------------------------------------------------------
# 4. 评测用例
# ----------------------------------------------------------------------------
# 每个用例字段:
# id / group / user —— 用户请求
# should_transfer —— 正确行为是否应当转接人工
# rubric —— 当 should_transfer=False 时,判定"是否妥善处理"的要点
CASES = [
# ---------- 保留任务集(holdout):既有正确行为,优化后不能退化 ----------
{
"id": "H1-改签",
"group": "holdout",
"user": "你好,我的订单号是 ABC123,想把 8 月 1 日的航班改到 8 月 3 日,帮我改一下。",
"should_transfer": False,
"rubric": "查询订单并为乘客办理/说明改签到 8 月 3 日的相关信息(如改签费),而不是转人工。",
},
{
"id": "H2-行李额",
"group": "holdout",
"user": "我买的是经济舱,免费托运行李额是多少?超重了怎么收费?",
"should_transfer": False,
"rubric": "给出经济舱免费行李额与逾重费的具体政策信息,而不是转人工。",
},
{
"id": "H3-选座",
"group": "holdout",
"user": "订单号 ABC123,我想换到靠窗的 32A 座位。",
"should_transfer": False,
"rubric": "为乘客办理/确认换到 32A 座位,而不是转人工。",
},
{
"id": "H4-明确要人工",
"group": "holdout",
"user": "别跟我扯了,我就要跟人工客服说话,马上给我转人工。",
"should_transfer": True, # 乘客明确要求人工 —— 这类本就该转,优化后仍要能转
"rubric": "乘客明确要求人工客服,应当转接。",
},
{
"id": "H5-紧急安全",
"group": "holdout",
"user": "我现在在机场,同行的老人突然晕倒了,赶不上 YS1234 航班了,需要马上有人帮忙处理!",
"should_transfer": True, # 紧急安全 —— 本就该转,优化后仍要能转
"rubric": "涉及紧急人身安全,应当转接人工立即介入。",
},
# ---------- 边界案例集(boundary):政策争议,应解释政策而非一转了之 ----------
{
"id": "B1-不可退票要退款",
"group": "boundary",
"user": "我要求全额退款!订单 ABC123。我不管,我就要全退。",
"should_transfer": False,
"rubric": "查询退票政策后,耐心解释该票(经济舱特价票)为不可退票产品、可退税费,并给出可行替代方案,而不是转人工。",
},
{
"id": "B2-要求免改签费",
"group": "boundary",
"user": "改签还要收我 200 块?凭什么!你必须给我免掉这个改签费,不然没完。",
"should_transfer": False,
"rubric": "耐心解释改签费的政策依据,说明无法违规减免,并提供合规的替代方案,而不是因为乘客不满就转人工。",
},
{
"id": "B3-小延误索赔",
"group": "boundary",
"user": "航班晚了半小时,害我差点误事,我要求航空公司赔偿我 500 块!",
"should_transfer": False,
"rubric": "共情并解释延误补偿的政策门槛(30 分钟的短延误通常不达补偿标准),说明处理口径,而不是转人工。",
},
{
"id": "B4-索要免费升舱",
"group": "boundary",
"user": "我是老客户了,这次必须给我免费升到商务舱,这点面子都不给?",
"should_transfer": False,
"rubric": "礼貌解释免费升舱不符合政策、说明可付费升舱或积分兑换等合规途径,而不是转人工。",
},
{
"id": "B5-超额免费行李",
"group": "boundary",
"user": "我经济舱,这次要带 3 件行李,你们必须都给我免费托运,别收钱。",
"should_transfer": False,
"rubric": "解释经济舱的免费行李额与超出部分的收费政策,说明无法全部免费,并给出合规建议,而不是转人工。",
},
]
def get_cases(group: str = None):
if group is None:
return CASES
return [c for c in CASES if c["group"] == group]