从 Function Calling 到前端动画:一个奶爸程序员的深夜探索实录
上个月刚换新东家,入职才两个月,就赶上了团队大重构。白天开会、改需求、被产品追着问“这个动效能下周上线吗?”,晚上回家还得陪俩娃洗澡讲故事。等他们睡了——差不多十点半——才是我的“第二班”时间。Vim 窗口一开,咖啡一杯,耳机一戴,这才真正进入 coder 模式。
最近我们项目要接入一个智能客服系统,后端用的是 Llama 系列模型(别问我为什么不是 GPT,公司合规要求+开源可控,懂的都懂)。但前端这边不能只是个“展示板”——用户点一下,弹个对话框,然后傻等几秒出结果?太 low 了。领导说:“得有点交互感,比如模型调用函数时,前端要有反馈动画。”
我心想:Function Calling 这玩意儿,不就是模型告诉前端“我要去查天气了”、“我要调支付接口了”,然后前端根据这些指令做 UI 反馈?
但问题是:怎么让前端优雅地处理这些动态指令?又怎么和 Llama 的输出无缝衔接?今天这篇,就聊聊我这两周在娃睡后、凌晨一点的 Vim 里折腾出来的方案。
起因:Llama 返回的不只是文本
我们用的是 Llama-3-8B-Instruct,通过 Ollama 部署在内网服务器。它的 Function Calling 能力其实挺强——只要你给它定义好工具(tools),它就能在生成回复的同时,附带一个 tool_calls 字段,告诉你它打算调哪些函数、参数是什么。
比如用户问:“帮我查下北京明天的天气。”
模型返回:
{
"content": null,
"tool_calls": [
{
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\", \"date\": \"2024-06-15\"}"
}
}
]
}
这时候,前端不能直接显示 null 啊!得知道“模型正在调用 get_weather”,于是我就想:能不能做个“思考中...”的微动效?甚至更进一步——根据函数名播放不同动画?比如查天气就飘朵云,支付就转个金币?
听起来简单,但真做起来,坑不少。
第一版:硬编码 + setTimeout,翻车现场
一开始我想偷懒,直接在 Vue 组件里写:
if (response.tool_calls) {
const funcName = response.tool_calls[0].function.name;
if (funcName === 'get_weather') {
this.showCloudAnimation();
setTimeout(() => this.fetchWeather(...), 800);
} else if (funcName === 'make_payment') {
this.showCoinSpin();
setTimeout(() => this.callPayment(...), 1000);
}
}
结果上线第二天就被测试小姐姐抓包:“你这个动画跟实际请求完全不同步!有时候动画播完了,数据还没回来,用户以为卡了。”
更惨的是,产品经理看了说:“以后要加新函数,你是不是每个都要改前端?这不符合‘配置即代码’的理念啊!”
我:……行吧,你说得对,是我图省事了。
第二版:用策略模式解耦动画与逻辑
痛定思痛,我决定搞个“函数-动画映射表”,配合一个通用执行器。
首先,定义一份前端可识别的函数清单(跟后端保持一致):
// functionRegistry.ts
export const FUNCTION_REGISTRY = {
get_weather: {
animation: 'cloudPulse',
delay: 600,
handler: async (args) => {
const res = await api.getWeather(args.city, args.date);
return `北京明天${res.temp}度,${res.desc}`;
}
},
make_payment: {
animation: 'coinRotate',
delay: 900,
handler: async (args) => {
await api.processPayment(args.amount);
return '支付成功!';
}
}
// 后续新增只需在这里加,不用动主逻辑
};
然后在聊天组件里统一处理:
<script setup>
import { FUNCTION_REGISTRY } from '@/utils/functionRegistry';
const handleModelResponse = async (response) => {
if (response.tool_calls?.length) {
const call = response.tool_calls[0];
const funcDef = FUNCTION_REGISTRY[call.function.name];
if (!funcDef) {
console.warn('Unknown function call:', call.function.name);
return;
}
// 触发动画
playAnimation(funcDef.animation);
// 等待动画延迟后再执行真实请求
await new Promise(resolve => setTimeout(resolve, funcDef.delay));
try {
const resultText = await funcDef.handler(JSON.parse(call.function.arguments));
addMessage({ role: 'tool', content: resultText });
// 再次发送给 Llama,让它基于工具结果生成最终回复
const finalResp = await sendToLlama([...messages.value, { role: 'tool', content: resultText }]);
addMessage(finalResp);
} catch (err) {
handleError(err);
}
} else {
// 普通文本回复,直接显示
addMessage(response);
}
};
</script>
这下清爽多了!新增函数?只要在 FUNCTION_REGISTRY 里配一行就行。动画和业务逻辑彻底分离,测试也夸我“这次同步性好多了”。
关键细节:如何让 Llama 知道工具调用完成了?
很多人忽略了一点:Function Calling 是多轮对话的一部分。你不能只调一次 API 就完事。
正确流程应该是:
- 用户提问 → 前端发给 Llama
- Llama 返回
tool_calls→ 前端执行对应函数 - 函数返回结果 → 前端把结果作为
role: "tool"的消息发回 Llama - Llama 再基于这个结果生成人类可读的最终回复
所以我们在前端必须维护完整的对话历史,包括 tool 类型的消息。否则 Llama 会懵:“我让你去查天气,你怎么没告诉我结果?”
我们的消息结构长这样:
const messages = [
{ role: 'user', content: '北京明天天气?' },
{
role: 'assistant',
tool_calls: [{ function: { name: 'get_weather', arguments: '...' } }]
},
{
role: 'tool',
content: '北京明天28度,多云'
},
// ← 下一轮 Llama 会基于以上三句生成最终回复
]
这点特别容易踩坑——我第一次没传 tool 消息回去,Llama 直接回了个:“好的,我已经帮你查了。” 结果啥数据都没有,用户一脸问号。
性能与体验权衡:动画延迟真的必要吗?
有人会问:为什么还要 setTimeout 延迟?直接立刻发起请求不行吗?
答案是:为了让用户感知到“AI 在思考”。
如果函数调用瞬间完成(比如本地缓存命中),用户看到的可能是:
用户:查天气
AI:北京明天28度
中间毫无过渡,用户会觉得“这 AI 怎么这么快?是不是假的?” 反而降低信任感。
所以我们故意加了 600~1000ms 的动画延迟,模拟“思考过程”。实测下来,用户满意度反而提升了——哪怕背后是毫秒级响应。
当然,对于真正耗时的操作(如支付),动画结束后立即发起请求,避免让用户等太久。
技术选型对比:为什么不用现成的 SDK?
其实市面上有 LangChain.js、LlamaIndex 前端库,它们内置了 Function Calling 处理。但我没用,原因有三:
| 方案 | 优点 | 缺点 | 是否采用 |
|---|---|---|---|
| 自研轻量逻辑 | 完全可控、无冗余、体积小 | 需自己处理边界情况 | ✅ 是 |
| LangChain.js | 功能全、社区支持好 | 包太大(+300KB)、过度封装 | ❌ 否 |
| 后端代理所有调用 | 前端无感、安全 | 失去交互灵活性、无法定制动画 | ❌ 否 |
我们项目对 bundle size 敏感(移动端占比高),而且交互体验是核心卖点,所以自研反而是最优解。
写在最后:奶爸的时间管理哲学
说实话,这套方案前后改了三版,都是在娃睡后两小时内搞定的。有时候写着写着,听见隔壁房间哭一声,立马切出去哄娃;回来继续写,靠的就是 Vim 的 session 恢复 + tmux 分屏。
技术探索不一定要大块时间。每天一小时,聚焦一个小问题,用真实项目驱动学习,反而比周末突击八小时更有效。
现在这个 Function Calling + 动画联动的方案已经上线两周,用户停留时长涨了 12%,产品还特意在周会上表扬了我(虽然紧接着就扔来三个新需求 😅)。
如果你也在带娃、加班、学新技术的夹缝中挣扎——别焦虑。每一个深夜的 commit,都是你对抗熵增的方式。
下次再聊怎么用 Web Animations API 做更流畅的 Llama 交互动效。先去给老二换尿布了,告辞!

评论 0