美团大模型算法二面:Function Call三连炮!
美团那场面试,差点把我整哭了
刚坐上那把椅子,后背还没贴稳。
面试官扫了一眼我的简历,看到“工具调用”那个项目经历,眼睛一下亮了。紧接着就甩出三个问题,一个比一个狠。我当时感觉自己像站在暴风雨里,雨点噼里啪啦砸脸上,还得硬撑着笑。
那是美团的二面,基础研发平台的智能体方向,JD上明明白白写着“优化function call、多智能体协调”。面试官一看就是做过真实项目的人,每个问题都问在点上。
后来我跟面友聊才知道,那三个问题早被江湖上叫作“Function Call连环炮”——网上很少人能讲清楚,但美团的人偏偏爱这么挖。
今天我把那些血泪全倒出来,你坐稳。
问题一:Function Call到底怎么训练出来的?
说到这儿我先问你一句——你是不是也觉得,调过几个商业大模型的API,用过tool_calls,就算懂Function Call了?
太天真了。
面试官想看的是,你脑子里有没有那条完整的训练链条。
翻翻Llama3的技术报告就明白了:工具调用能力根本不是预训练阶段学的,是在post-training阶段硬生生塞进去的。怎么塞?靠反复SFT加DPO迭代,一点点打磨。
有几个点,跟外面传的完全不一样:
标注员只负责给assistant的推理过程打分,完全不碰tool信息。为什么?判断工具调用对不对是纯技术活,普通标注员一碰就容易出噪音。所以正确性交给规则或自动验证,人工只评估回复的自然度和有用性。
另外,他们不做rejection sampling。很多团队把这当宝贝,觉得能提升tool use效果。结果Llama3内部测试发现没什么明显收益,直接砍了。这就是大厂的态度——不迷信方法,只认效果。
还有,数据的难度是逐渐加码的。先标单轮tool use对话,让你先熟悉简单场景;然后是多轮对话里夹着工具调用;最后一层最变态——多步工具调用加数据分析。就像打游戏,先在新手村练级,再慢慢打BOSS。
说说我自己的项目当时怎么做的。
我用Qwen2.5-7B做微调,基本照着这个思路来。数据主要用了Glaive的glaive-function-calling-v2-sharegpt,覆盖挺全,但有个硬伤:工具描述大多是英文。国内场景要的是中文加本地API。
没办法,只能自己造。
我手写了一批SFT数据,每条包含四个核心字段:
- **system**:工具列表,JSON Schema格式,一个字段都不能错。
- **user**:真实的用户指令,比如“帮我看一下北京今天天气好不好,然后发邮件告诉张总一声”。
- **assistant**:先思考再调工具,里面藏着tool_calls。
- **tool**:工具执行后返回的结果。
踩过的坑,说出来都是泪。
第一个坑:工具描述写得太敷衍。 我有个“get_weather”函数,description就写了“获取天气”。结果模型后来连“获取空气质量”也去调这个函数。用户问“北京今天空气怎么样”,它啪地调了个天气函数——这谁受得了。后来我规定:description必须写清楚输入输出、使用条件,甚至得告诉模型“This tool returns current temperature and weather condition for a given city, NOT air quality。”连不许干什么都得说明。
第二个坑:参数类型打架。 Qwen的tokenizer对JSON嵌套特别敏感。properties里某个字段写了“type: string”,结果模型传了个数字进去,整个输出就乱了。后来我在post-processing里硬加了一层json schema校验,不合规就重新来。
第三个坑:多轮对话里,历史的tool_call信息像个漏水的桶。 如果不把tool_call和tool_response拼回历史,模型下一轮就会失忆——刚才调过什么全忘,然后又开始瞎调用。后来我严格按照ChatML格式,在每个turn的assistant回复里都保留<|tool_call|>标记,才算是稳住。
问题二:Function Call的文本格式,套路比你想象的深
这个问题有意思。
说出来你可能不信,大多数人栽就栽在这里。都以为把工具schema往system prompt里一塞就完事——简单粗暴,万无一失。
错。
不同模型有自己的“输入方言”,就像不同地方的人说不同口味的普通话。
我项目里跑通的那套格式,基于ChatML,大概长这样:
<|im_start|>system
You have access to the following tools:
[{"type":"function","function":{"name":"func_add","description":"计算两个数字的和","parameters":{"type":"object","properties":{"x1":{"type":"number","description":"第一个数字"},"x2":{"type":"number","description":"第二个数字"}},"required":["x1","x2"]}}}]
<|im_end|>
<|im_start|>user
帮我算一下 125679 加上 234519 是多少?
<|im_end|>
<|im_start|>assistant
没问题,我来帮你计算。
<|tool_call|>{"name":"func_add","arguments":{"x1":125679,"x2":234519}}<|tool_end|>
<|im_end|>
<|im_start|>tool
{"ans":360198}
<|im_end|>
<|im_start|>assistant
结果是 360198。
<|im_end|>看着简单?有几个细节我折腾了好几宿才弄明白。
tool_calls必须跟在assistant的content之后。如果模型先说一句话再调工具,格式必须严丝合缝。很多资料把tool_calls单独当字段处理,但训练时得把它序列化成文本,格式一乱模型学到的就是浆糊。
特殊token必须统一,不能今天用明天换。我用的是<|tool_call|>和<|tool_end|>,Qwen官方推荐那套。换着用,下次微调模型就飘。最好的办法是一开始就定死模板,然后冻住,谁也别碰。
工具描述里的description,最好加上使用场景。比如“该工具只能在用户主动要求时调用”——这样能减少莫名其妙的幻觉调用。模型有时候自己给自己加戏,不约束就会乱来。
最后给你留个小心机:面试官可能会追问“不同模型的Position Embedding不一样,会不会影响tool call格式?” 这事我琢磨过。LLaMA用RoPE,Qwen用SwiGLU加RoPE,但模板是自己定的,理论上一套格式可以通用。唯一要防的是max length——多工具调用时,token序列会急剧膨胀。一次调三个工具,加上返回结果,随随便便就超8k。所以我一般在推理时动态调整prompt的truncation策略,优先保最后几轮对话。
问题三:怎么让模型真正“看懂”下游工具?
这问题最落地。
面试官真正想知道的是:你怎么把现实世界里那些乱七八糟的REST API、数据库查询、第三方SDK,变成模型能理解的东西?
我当时从四个方面回答。
接口标准化
每个工具必须说清楚四件事:name、description、parameters(类型、枚举值、是否必填)。但description不是随便写写,要让模型知道什么时候该用这个工具。
比如我的“send_email”:
{
"name": "send_email",
"description": "Send an email to a recipient. Use this only when the user explicitly asks to send an email. Don't call it just for checking inbox.",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "收件人邮箱"},
"subject": {"type": "string", "description": "邮件主题"},
"body": {"type": "string", "description": "邮件正文"}
},
"required": ["to", "subject"]
}
}注意到我在description里专门加了一句“Don't call it just for checking inbox。”——这是血泪教训。模型有时会自作主张,你不拦住它,它就闹。
处理“幻觉工具”
自然语言理解里最让人头疼的,是模型会把用户不完整的指令自作主张补全,然后编一个根本不存在的工具名。用户说“查一下物流”,模型可能啪地调出一个“check_logistics”——问题是你代码里根本没有这个函数。
怎么办?我在system prompt里加了一句硬命令:“如果你不确定该用哪个工具,不要猜,给我反问用户。”——简单粗暴,但管用。
工具返回结果要结构化
工具返回的结果不能直接甩自然语言回去。比如天气API返回的是JSON,你得保留JSON结构,或者至少保证key是稳定的,否则模型下一轮解析就会乱套。
我踩过一个坑:tool response返回了一段中文散文,结果模型直接拿它当assistant回复,不再调用后续工具。后来我立了规矩:tool response必须是纯JSON,并且在训练时让模型学会把JSON转换成自然语言。
多工具依赖关系的建模
面试官后来追了一句——那“多步工具调用”怎么办?用户说“帮我总结一下昨天邮件里的会议安排,然后加到日历里”。这涉及两个工具,还有前后依赖关系。
我当时的做法是:让模型在同一个assistant回复里发起多个<|tool_call|>,然后按顺序等结果。但模型经常搞混顺序。后来我加了一个简单的CoT提示:“首先获取邮件内容,然后从内容里提取事件信息,再调用日历工具。”效果提升很明显。
最后,说点真心话
给你几个能让你在面试里更出众的建议:
1. Llama3的technical report,第2.3节,去读透。 别只看二手解读。原文关于tool use的训练数据构造写得清清楚楚。很多人死记硬背“SFT+DPO”,但为什么他们不做rejection sampling?你答得上来吗?面试官一追问,你就露馅了。
2. 自己动手,造一个完整的function calling微调数据集。 不用太复杂,选三五个API就行——天气、计算器、翻译,单轮多轮混着来。用LlamaFactory跑一遍SFT,你马上就会明白什么叫“数据格式不对,训练全白废”。
3. 重点关注工具调用的错误恢复机制。 这是区分普通工程师和高级工程师的分水岭——当工具返回错误信息(API超时、参数非法),模型应该怎么办?大部分方案就是重新调用一次。但应该再试一次还是换工具?美团的人特别在意这个,因为智能客服的容错率太低了。
4. 别光看论文,去看代码。 Hugging Face的transformers里有apply_chat_template的实现,trl库里有DPO的训练示例。亲手跑一遍,你才会知道tool_calls字段到底该存成dict还是字符串,tokenizer会不会把你的函数调用截断了。
最后说句真话:美团那三连炮虽然刁钻,但回过头想,它们确实是做Agent落地必须翻过去的坎。
那一个小时,是我职业生涯里成长最快的一小时。
如果有人提前告诉我这些坑,我至少能少翻两天垃圾文献,少熬三个通宵。
现在我把这些摔打记录都摊给你了。下回面试遇到function call的题,别只聊API调用了——往数据构造、模板设计、错误处理上多聊几句,面试官的眼睛会亮的。
有些路,只有摔过才会记住。但希望这篇东西,能让你少摔几跤。
能让你在面试官面前发光的东西,从来不是API调参的技巧,而是你对整条链路的心跳。
读者评论 2