大模型第十三天后端代码学习笔记
第十三天讲的是提示词工程算岗位匹配度、还有 function calling(就是让大模型能调外部工具)。
项目里相关文件主要是这几个:
app/services/job_service.py:算岗位匹配度的地方llm/case5.py:function calling 入门,用天气查询当例子llm/case6.py:function calling 接真实的学历验证接口,还带了 Redis 缓存
1 用提示词工程算岗位匹配度
这个其实没用到 function calling,就是纯提示词(prompt)工程。在job_service.py里有个resume_submission_detail方法,它的活儿是:HR 在后台看某个人投某个岗位的详情时,顺便让大模型分析一下"这个人跟这个岗位有多匹配"。
它做的事情分两步:
第一步,把岗位信息和简历信息从数据库里捞出来。岗位这边有职位名称、工作地点、薪资、经验要求、学历要求、性别要求、职位描述、任职要求;简历那边更杂,有基本信息、期望职位、期望薪资、工作经历、项目经历、教育经历、专业技能、语言能力、证书荣誉……反正能塞的都塞进去了。
第二步,把这些拼成一个超级长的 prompt 丢给大模型。prompt 大概长这样(我截重点):
##角色设定: 你是一个经验丰富的人力资源专家 ##任务描述: 根据求职者简历内容和岗位的职位描述,分析岗位匹配度和理由 ##输入数据: 1.岗位的职位描述: 1.1:职位名称:{job.job_name} 1.2:工作地点: {job.work_location} ...(一堆岗位字段) 2.求职者简历内容: 2.1:求职者性别(1-男,2-女):{resume_basic_info.gender} ...(一堆简历字段) ##输出数据: 以JSON格式输出,包含以下字段: job_matching_degree:岗位匹配度(0-100)百分比 matching_reason: 重要:禁止输出任何思考、推理、标签,直接输出JSON结果,不要输出其他文字。 ## 输出示例: {"job_matching_degree": "80%", "matching_reason": "..."}调模型那段就很朴素:
client=OpenAI(api_key=os.environ['DASHSCOPE_API_KEY'],base_url="https://ws-89t66lrlxx0rdobw.cn-beijing.maas.aliyuncs.com/compatible-mode/v1")completions=client.chat.completions.create(model='qwen-plus',messages=[{"role":"user","content":prompt}])res=completions.choices[0].message.contentreturnres说白了就是:把所有信息塞进一个 user 消息,让大模型当 HR 专家,按指定 JSON 格式吐出匹配度和理由。
我觉得这块最值得学的点是那个"输出格式约束"——你不光告诉它要输出 JSON,还给了输出示例,并且明确说"禁止输出思考过程,直接给 JSON"。不然大模型很喜欢先啰嗦一大段"好的,我来帮你分析",前端解析就麻烦了。
2 function calling 是干嘛的
先说为啥需要它。前面算匹配度那种,大模型自己就能干,因为答案在它脑子里(它是被训练过的)。但有些事它干不了,比如"北京今天天气咋样"——它训练数据里没有实时天气,它就瞎编。
function calling 就是给大模型装"外挂":你提前告诉它"你有一堆工具可以用,每个工具是干嘛的、要什么参数"。大模型看到用户问题后,如果觉得需要查天气,它就告诉你"我要调get_current_weather这个工具,参数是北京",然后你的程序真的去查天气,把结果再喂回给大模型,大模型最后组织成自然语言回答用户。
一句话:大模型负责"想调哪个工具、给什么参数",程序负责"真去执行工具",各干各的。
3 function calling 的工作流程
整个流程五步,我画个大概:
- 你定义好
tools(工具清单),跟着消息一起发给大模型 - 大模型回你:我要调
xxx工具,参数是yyy(返回里带着tool_calls) - 你的程序根据
tool_calls真的去执行那个工具函数,拿到结果 - 你把工具结果拼成一条
role: "tool"的消息,再丢回给大模型 - 大模型拿到工具结果,组织成最终回答返回
关键就是第 4 步那条role: "tool"的消息,它必须带上tool_call_id,大模型才知道这个结果是回给哪次工具调用的。
4 function calling 入门案例(天气查询)
看llm/case5.py。先定义工具清单:
tools=[{"type":"function","function":{"name":"get_current_weather","description":"当你想查询指定城市的天气时非常有用。","parameters":{"type":"object","properties":{"location":{"type":"string","description":"城市或县区,比如北京市、杭州市、余杭区等。",}},"required":["location"],},},},]然后弄个假的天气查询函数(真实项目里这里才是真去调天气 API):
defget_current_weather(arguments):weather_conditions=["晴天","多云","雨天"]random_weather=random.choice(weather_conditions)location=arguments["location"]returnf"{location}今天是{random_weather}。"发请求的时候把tools带上:
defget_response(messages):completion=client.chat.completions.create(model="qwen-plus",extra_body={"enable_thinking":False},messages=messages,tools=tools,)returncompletion然后就是判断大模型有没有要调工具:
ifcompletion.choices[0].message.tool_callsisNone:print("不需要调用工具")print(completion.choices[0].message.content)else:print("需要调用工具")tool_calls=completion.choices[0].message.tool_callsfortool_callintool_calls:tool_id=tool_call.idfunc_name=tool_call.function.name func_arguments=tool_call.function.arguments function_mapping={"get_current_weather":get_current_weather}tool_result=function_mapping[func_name](json.loads(func_arguments))tool_message={"content":tool_result,"role":"tool","tool_call_id":tool_id}messages.append(tool_message)completion=get_response(messages)print(f"最终的结果是:{completion.choices[0].message.content}")这里function_mapping是个字典,把工具名映射到真正的 Python 函数。大模型只告诉你要调哪个、给啥参数,具体执行是你这边function_mapping[func_name](...)干的。
这个 case5 有两个小坑(case6 里修好了):
- 第 56 行
messages.append(user_messages),而user_messages本身是个列表,append 进去会变成"列表套列表",应该用messages.extend(user_messages) - 第 59 行
messages.append(completion.choices[0].message)直接把消息对象塞进去,规范点应该用completion.choices[0].message.model_dump()转成字典
5 function calling 接学历验证接口
llm/case6.py是真实业务了——接"梦远学历"验证接口(www.apimy.cn)。这次定义了两个工具,天气和学历验证:
defacademic_credential_verification(arguments):vcode=arguments['vcode']key=f"boss:llm:academic_credential_verification:{vcode}"redis_verification_data=r.get(key)ifredis_verification_dataisNone:BASE_URL=f"https://www.apimy.cn/api/xxw/bgcx?key={os.getenv('API_KEY')}"response=requests.post(BASE_URL,data={"vcode":arguments["vcode"]},timeout=30)response.raise_for_status()data=response.json()r.set(key,json.dumps(data,ensure_ascii=False))returnjson.dumps(data,ensure_ascii=False)else:returnredis_verification_data这块比 case5 多了两样好东西:
一是真的发 HTTP 请求。用requests.post去调第三方学历验证接口,拿到 JSON 结果。
二是加了 Redis 缓存。用验证码vcode当 key,先r.get查缓存,没有才去调接口,调完r.set存起来。同一个验证码第二次查就直接走缓存,不用再打第三方接口——省次数也快。
还有 case6 把 case5 那两个坑都修好了:messages.extend(user_messages)(用 extend 不是 append)、messages.append(completion.choices[0].message.model_dump())(用 model_dump 转字典)。
调用流程跟 case5 一样,也是tool_calls→ 执行函数 → 塞role:"tool"消息 → 再请求一次拿最终答案。
这些怎么接到 FastAPI 接口里
课程里 case5/case6 还是脚本,真要变成后端接口,套路跟前面多轮对话那章差不多:
- 把 tools 和函数定义放到接口文件里
- 写一个路由,比如
@router.post("/verify_academic"),接收user_id、vcode这种参数 - 把函数调用那段包成生成器,配合
StreamingResponse就能流式输出(前面第十二章做过) - 会话上下文用 Redis 存,key 设计成
boss:llm:academic:{user_id}:{session_id}这种
要注意的是,function calling 这套"多轮"比普通对话多一步:每轮都得把role:"tool"的消息正确塞回 messages,否则大模型不知道工具结果对应哪次调用,容易乱。
- 提示词工程算匹配度:把岗位+简历拼成大 prompt,约束输出 JSON,让大模型当 HR 打分。简单但实用。
- function calling:大模型管"调哪个工具、给啥参数",程序管"真执行",结果用
role:"tool"消息回传。case5 是玩具例子(天气),case6 是真实例子(学历验证)还带了 Redis 缓存。
代码都不难,难的是理解那条 messages 链路怎么一步一步拼对的。我也是对着打印出来的completion.model_dump_json()一点点看才搞明白的。