Day 63 / 共 68 天 · 阶段 12 实战求职
作品集② 多工具 Agent(会动手做事)
昨天(Day 62)做的 RAG 助手只会"查资料答题"。今天做第二件作品——让 Agent 真正动手办事:查天气、查库存、发邮件。核心是把 Day 29 的 Function Calling 和 Day 30 的安全工具设计、结构化错误处理落进一个能演示的项目。明天(Day 64)把两件作品升级成"研究→写作→审校"的多智能体工作流。
📍 你在阶段 12(实战求职 D62-68)的位置
D62 作品集①RAG→
D63 作品集②工具→
D64 作品集③多智能体→
D65 开源包装→
D66 简历→
D67-68 面试冲刺
💡 用一个类比兜住今天(今天全程沿用「新来的私人助理」的世界观)
多工具 Agent = 一位刚到岗的私人助理。工具 = 你交给他的一串"办事电话"(查天气热线、库存系统、公司邮箱);工具描述(schema) = 每个电话旁贴的便签("这个号干啥用、要报哪几个信息");Function Calling = 助理听懂你的话后,自己决定"该拨哪个电话、报什么参数";你执行 + 回填 = 电话那头(真正的 API)办完把结果告诉助理;错误处理 = 电话打不通时,助理不能当场愣住/瞎编,而要礼貌地说"这条线忙,我换个方式";护栏 = "发邮件这种大事,先请示你一句再发"。今天你在训练一位靠谱的新助理。
L01
定需求:从"会说"到"会做"
🤔 痛点RAG 助手再聪明,也只是"嘴皮子"——问它答它。真实工作里的价值往往是"帮我把事办了":查到天气顺手提醒带伞、查到缺货自动通知采购。不会调工具的 Agent,就是个高级搜索框。
💡 本质还是先填需求卡(Day 62 的习惯)。多工具 Agent 的需求卡多一栏:它能调哪几个工具、每个工具的边界。想清楚"助理能替我打哪几个电话",项目就有了骨架。
📝 举个例子:填好的需求卡
项目名:出差小助手
用户:经常出差的同事
痛点:出发前要分别查天气、查酒店库存、还要发通知,来回切好几个系统
工具三件套:①查天气(给城市→返回天气) ②查库存(给酒店/日期→返回余房) ③发邮件(给收件人/内容→发出)
护栏:发邮件属"危险动作",执行前必须让用户确认
成功标准:20 个混合任务,正确选对工具并完成 ≥ 85%
用户:经常出差的同事
痛点:出发前要分别查天气、查酒店库存、还要发通知,来回切好几个系统
工具三件套:①查天气(给城市→返回天气) ②查库存(给酒店/日期→返回余房) ③发邮件(给收件人/内容→发出)
护栏:发邮件属"危险动作",执行前必须让用户确认
成功标准:20 个混合任务,正确选对工具并完成 ≥ 85%
👶 一定要三个工具吗?不是硬性。但至少 2~3 个,才能展示"Agent 会挑工具"这个核心能力。只有一个工具,模型无脑调它就行,体现不出"决策"。有多个工具时,模型得根据你的话判断"这句该拨哪个电话"——这才是 Function Calling 的看点,也是面试想看的。
L02
工具三件套:给每个电话贴好便签(schema)
🤔 痛点模型不会读心。你光写个
def send_email(...),模型根本不知道"这函数干啥、什么时候该用、要传哪些参数"。它只能看到你给的文字描述。描述含糊,模型就选错工具、传错参数。💡 本质每个工具 = "函数本体" + "一张说明便签(schema)"。便签用 JSON 写清:名字、这工具干啥(给模型看的说明)、要哪些参数、每个参数啥意思。便签写得越清楚,助理越不会拨错电话(呼应 Day 30 安全工具设计)。
# 工具本体:三个普通函数(真实项目里换成调真 API;这里先用假数据跑通)
def get_weather(city: str) -> dict:
return {"city": city, "weather": "小雨", "temp": 18} # 假装查了天气
def check_stock(hotel: str, date: str) -> dict:
return {"hotel": hotel, "date": date, "rooms_left": 3} # 假装查了库存
def send_email(to: str, subject: str, body: str) -> dict:
return {"status": "sent", "to": to} # 假装发了邮件
// 给每个工具配一张"说明便签"(schema),交给模型看
[
{
"name": "get_weather",
"description": "查询某个城市当前天气,用户问天气/要不要带伞时调用",
"parameters": {
"type": "object",
"properties": { "city": {"type": "string", "description": "城市名,如 北京"} },
"required": ["city"]
}
},
{
"name": "send_email",
"description": "发送一封邮件。属危险操作,执行前应先向用户确认",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "收件人邮箱"},
"subject": {"type": "string"},
"body": {"type": "string", "description": "邮件正文"}
},
"required": ["to", "subject", "body"]
}
}
]
📝 举个例子:好便签 vs 坏便签
坏:
好:
"description": "邮件相关"——模型一脸懵,查邮件?写邮件?发邮件?好:
"发送一封邮件。属危险操作,执行前应先向用户确认"——既说清用途,又把护栏写进描述里。schema 是你和模型之间唯一的"说明书",写它比写函数本身更需要用心。L03
主循环:模型选电话 → 你拨 → 回填 → 再决策
🤔 痛点Function Calling 到底怎么"动"起来?模型不是直接就把邮件发了——它只会说"我想调 send_email,参数是这些"。真正拨电话(执行)的还是你的代码。这个来回,新手最容易懵。
💡 本质一个循环转圈(呼应 Day 29、Day 33 的主循环):①把用户的话 + 工具便签发给模型 → ②模型回一个"我要调哪个工具+参数" → ③你的代码真去执行那个函数 → ④把结果回填给模型 → ⑤模型看着结果,要么再调下一个工具,要么给最终答案。转到没有新工具要调为止。
图注:模型只负责"决定调谁、传什么";真正执行的永远是你的代码。转圈直到模型不再要工具。
# 主循环骨架(伪代码级简化,看懂流程即可)
tools = {"get_weather": get_weather, "check_stock": check_stock, "send_email": send_email}
messages = [{"role": "user", "content": "我明天去上海出差,天气咋样?"}]
for _ in range(5): # 循环上限:最多转 5 圈,防死循环(Day32)
resp = client.chat.completions.create(
model="gpt-4o", messages=messages, tools=SCHEMAS) # 把便签一起发过去
call = resp.choices[0].message.tool_calls # 模型是否要调工具?
if not call: # 不调了 → 说明它要给最终答案
print(resp.choices[0].message.content); break
name = call[0].function.name # 它想调哪个
args = json.loads(call[0].function.arguments)# 它给的参数
result = tools[name](**args) # ③你的代码真去执行
messages.append({"role": "tool", "content": json.dumps(result)}) # ④回填结果
L04
错误处理 & 护栏:助理不能愣住,也不能闯祸
🤔 痛点①真实 API 会失败(网络抖、参数错、限流)。工具一
raise 异常,整个 Agent 当场崩溃——助理"啪"地愣住,后面全废。②模型可能自作主张把没确认的邮件发出去,闯大祸。💡 本质两条铁律(Day 30 安全工具 + Day 54 护栏):①工具永远不抛异常,而是返回结构化的"错误信息",让模型看到"这条线打不通",自己换个法子;②危险动作(发邮件/花钱/删数据)先审批,不到用户点头绝不真执行。助理可以办事,但不能愣住、更不能背着你闯祸。
# 铁律1:工具内部 try/except,把错误"变成返回值"而不是崩溃(Day05 + Day30)
def check_stock(hotel: str, date: str) -> dict:
try:
# ... 真去调库存 API ...
return {"ok": True, "rooms_left": 3}
except Exception as e:
return {"ok": False, "error": f"库存系统暂时打不通:{e}"} # ← 结构化错误,不 raise
# 模型收到 {"ok": False, ...} 会知道"这步没成",可改口"稍后重试"或换工具
# 铁律2:危险动作加"审批闸"——执行前先问用户(人在环,Day41/Day54)
def send_email(to, subject, body, confirmed=False):
if not confirmed:
return {"ok": False, "need_confirm": True, # 先不发,把草稿退回请示
"preview": f"将发给 {to}:{subject}"}
# 只有 confirmed=True 才真的发
return {"ok": True, "status": "sent"}
👶 小白:工具出错就返回错误,模型看不懂那串 error 怎么办?
👨🏫 老师:恰恰相反,模型很擅长读这种"人话错误信息"。所以 error 里别只写 Error 500,要写模型能据此决策的自然语言,比如"库存系统超时,建议稍后重试或改问其他酒店"。模型读到就会顺势重试或换招,而不是把一串天书原样甩给用户。这就是"结构化错误"比"直接崩溃"高级的地方——它把失败变成了可处理的信息。
L05
评测:任务完成率 + 工具选对率
🤔 痛点RAG 评的是"答得对不对"。工具 Agent 更复杂:它可能"话说得漂亮,但工具选错了 / 参数传歪了 / 该确认没确认"。只看最后一句回复,根本发现不了这些问题。
💡 本质工具 Agent 的评测(呼应 Day 51 指标体系)看两把尺子:①工具选对率——面对一句话,有没有挑对该调的工具、参数对不对;②任务完成率——整件事到底办成没。做法还是攒 golden set,只不过每条的"期望"变成"该调哪个工具 + 最终该达成什么"。
# 评测骨架:每条用例记录"该调的工具"和"该完成的事"
golden = [
{"q": "上海明天天气?", "expect_tool": "get_weather", "expect_arg": "上海"},
{"q": "把行程发给张总", "expect_tool": "send_email", "need_confirm": True},
{"q": "外滩饭店后天还有房吗?", "expect_tool": "check_stock", "expect_arg": "外滩饭店"},
]
right_tool = 0
for c in golden:
tool_name, args = run_agent_first_call(c["q"]) # 跑一次,看它第一步选了啥工具
ok = (tool_name == c["expect_tool"]) # 工具选对没
right_tool += ok
print(("✅" if ok else "❌"), c["q"], "→", tool_name)
print(f"工具选对率:{right_tool}/{len(golden)} = {right_tool/len(golden):.0%}")
📝 举个例子:评测揪出的典型 bug
跑评测发现"把行程发给张总"这条,Agent 直接把邮件发了(
need_confirm 没生效)。定位到是 schema 描述里没强调"先确认" → 补上描述 + 代码审批闸 → 再测,危险动作 100% 先请示。"我用评测发现 Agent 会绕过审批,并修复了它"——这种安全意识的故事,面试极其加分(呼应 Day 49 评测最重要)。L06
部署 + 可复用骨架
🤔 痛点怎么把这个"会办事的助理"变成可演示的作品?结构上它和 RAG 助手很像(FastAPI + Docker),但多了工具目录和审批环节,得组织清楚,别把工具和主循环搅成一团。
💡 本质好架构 = 工具、schema、主循环各归各位。工具单独放一个文件夹(将来加新工具只改这里)、schema 单独一份、主循环只管"转圈"。这样别人一看目录就懂,加工具也不怕碰坏别的地方(高内聚低耦合的直觉)。
# 可复用项目骨架
tool-agent/
├── tools/
│ ├── weather.py # 查天气工具 + 它的错误处理
│ ├── stock.py # 查库存工具
│ └── email.py # 发邮件工具(含审批闸)
├── schemas.py # 所有工具的"说明便签"(L02)
├── agent.py # 主循环:选工具→执行→回填(L03)
├── app.py # FastAPI 暴露 /chat 接口(Day09)
├── eval.py # 评测:工具选对率 + 任务完成率(L05)
├── golden.json # 评测用例
├── requirements.txt
├── Dockerfile # 打包(Day57)
└── README.md # 说明 + 效果数字 + demo(Day65)
👶 工具太多了记不住怎么办?这正是框架(阶段 7)和 MCP(Day 31)要解决的。手写循环工具一多就乱,这时可以搬出 LangGraph/CrewAI 把流程画成图,或用 MCP 把工具做成"标准插头"即插即用。但作品集里,先用手写循环把 3 个工具跑通,反而更能证明你懂原理——框架是锦上添花,原理是地基。想深入编排,去看下面两套精讲(选学,学完回来继续 Day 63):
🔗 深入《LangGraph 20 天精讲》→
🔗 深入《Claude Code 源码精讲》看真实工具设计 →
L07
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- 为什么至少要 2~3 个工具,才体现出"Agent 会挑工具"?
- 工具的 schema(说明便签)为什么比函数本体还重要?好便签长啥样?
- 画出 Function Calling 主循环:模型选工具→谁执行→回填→再决策?
- 两条铁律:工具为什么"返回结构化错误"而不 raise?危险动作为什么先审批?
- 工具 Agent 评测看哪两把尺子?(工具选对率 + 任务完成率)
✋ 动手 10 分钟:写两个假工具 + 一个"手动主循环"
不接真 API、不用大模型,先用手模拟一遍主循环,把流程刻进肌肉:
# 1) 两个假工具,其中"发邮件"带审批闸
def get_weather(city): return {"ok": True, "weather": "小雨", "temp": 18}
def send_email(to, body, confirmed=False):
if not confirmed:
return {"ok": False, "need_confirm": True, "preview": f"给{to}:{body}"}
return {"ok": True, "status": "sent"}
# 2) 你来扮演"模型",手动决定调哪个工具(体会主循环的每一步)
print(get_weather("上海")) # 第1圈:查天气
draft = send_email("张总", "行程见附件") # 第2圈:想发邮件 → 被审批闸拦下
print(draft) # need_confirm=True,先给你看草稿
print(send_email("张总", "行程见附件", confirmed=True)) # 你点头后才真发
能说清"每一步是模型在决策还是我的代码在执行、哪一步被审批闸拦住",今天就通关了。
明日预告 · Day 64:作品集第三件,也是最能串起全课的压轴项目——多智能体工作流。让"研究员 + 写手 + 审校"三个 Agent 协作产出一篇报告(Day 48 的实战),再给它接上评测(阶段 9)和可观测(阶段 10)。RAG(作品①)、工具(作品②)、多智能体协作全部汇到这一件里,做完你的作品集就成型了。