ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从会聊天到会干活:提示词工程与工具调用实战指南

从会聊天到会干活:提示词工程与工具调用实战指南 1. 从“会聊天”到“会干活”的分水岭到底在哪很多人第一次接触大模型都是从聊天开始的。你问它一句“帮我写个周报”它噼里啪啦给你输出一大段看着挺像那么回事。但如果你真让它去干一件具体的事比如“帮我查一下明天北京的天气然后根据温度推荐一套穿搭”它就开始露馅了——要么编一个天气数据给你要么告诉你“我无法获取实时信息”。这个体验上的巨大落差就是“会聊天”和“会干活”之间的鸿沟。我自己在这个坑里蹲了很久。早期做项目的时候天真地以为只要提示词写得足够好模型就能完成所有任务。后来发现提示词再精妙模型本身的能力边界是锁死的它没有实时数据、不能操作外部系统、不能执行代码、不能查数据库。你让它写一段SQL没问题但你让它去数据库里跑一下再把结果拿回来它就无能为力了。工具调用Tool Calling / Function Calling就是用来填平这条鸿沟的。它的核心思路非常朴素模型本身不执行任何外部操作但它可以“告诉”你的程序“我现在需要调用某个函数参数是这些”然后你的程序去执行再把结果喂回给模型模型基于结果继续推理或生成最终回答。整个过程里模型扮演的是一个“调度中枢”的角色真正干活的是你写的那些函数。这篇文章我会把提示词设计和工具调用这两块拆开揉碎了讲。从最基础的提示词结构到工具调用的完整链路再到实际项目里踩过的坑和解决方案都会覆盖到。不管你是刚接触大模型开发的新手还是已经在做企业级应用的老手应该都能从里面找到对自己有用的东西。2. 提示词工程不是“会说话”就行2.1 提示词的本质是约束搜索空间很多人把提示词理解成“跟模型说话的方式”这个理解不能说错但太表面了。更准确地说提示词是在约束模型的输出空间。大模型本质上是一个概率分布你给它一个输入它会在所有可能的输出里按概率采样。提示词的作用就是把这个采样空间缩小到你想要的范围内。举个例子你输入“写一首诗”模型可能会写出五言绝句、七言律诗、现代诗、打油诗甚至给你来一段散文。但如果你输入“写一首七言绝句押平水韵主题是秋天思乡每句七个字共四句”输出空间就被大幅压缩了。这就是提示词的核心价值——通过增加约束条件让模型的输出更接近你的预期。我通常把提示词拆成四个层次来设计角色层告诉模型它是谁。“你是一名有十年经验的Python后端工程师”和“你是一个助手”产出的代码质量差距是肉眼可见的。任务层明确要做什么。动词要具体“分析”不如“列出三个优点和三个缺点”“优化”不如“将时间复杂度从O(n²)降到O(n log n)”。约束层格式、长度、风格、禁忌。比如“用JSON输出”、“不超过200字”、“不要使用专业术语”。示例层给一两个输入输出样例。这是最有效的提示词技巧之一后面会详细讲。2.2 几个真正有效的提示词技巧网上关于提示词技巧的文章多如牛毛但很多都是玄学。我挑几个在实际项目中反复验证过有效的说一下。Few-Shot示例比任何形容词都管用。你说“用专业的语气”模型理解的专业和你的专业可能不是一回事。但你给两个示例它立刻就能对齐。我在做一个合同信息抽取的项目时一开始用各种形容词描述输出格式效果都不稳定。后来直接给了三个“输入文本→输出JSON”的示例准确率从60%多直接拉到90%以上。思维链Chain of Thought要慎用。“让我们一步一步思考”这句话在推理任务上确实有效但在信息抽取、格式转换这类任务上反而会让模型输出一堆废话。我的经验是需要多步推理的数学题、逻辑题、复杂决策用思维链简单的分类、抽取、改写不用。负面指令不如正面指令。“不要编造数据”不如“如果信息不足输出‘未找到相关信息’”。“不要用markdown”不如“用纯文本段落输出”。模型对正面指令的遵循度明显更高。系统提示词和用户提示词要分工。系统提示词定角色、定规则、定输出格式用户提示词给具体任务和输入数据。不要把什么都塞进用户提示词里那样每轮对话都要重复一遍浪费token还容易冲突。2.3 提示词设计的常见误区说几个我踩过的坑。第一个坑是提示词过长导致关键信息被稀释。早期我写提示词恨不得写两千字把所有能想到的规则都塞进去。结果模型反而抓不住重点因为注意力被分散了。后来我学会把提示词控制在合理长度内核心规则不超过十条每条不超过两句话。第二个坑是用提示词去解决模型能力不足的问题。比如让模型做复杂的数学计算你提示词写得再好它该算错还是算错。这种时候正确的做法是调用计算器工具而不是死磕提示词。第三个坑是忽略不同模型的提示词差异。同一个提示词在GPT-4上表现很好换到另一个模型上可能完全不行。每个模型在训练时用的指令格式不一样对系统提示词的敏感度也不一样。做项目时如果涉及多模型切换提示词一定要做适配测试。3. 工具调用让模型长出“手脚”3.1 工具调用的完整链路工具调用听起来很玄乎但拆开来看其实就四步定义工具你用JSON Schema描述每个工具的名称、功能、参数。比如一个查天气的工具参数是城市名和日期。模型决策你把用户的问题和工具列表一起发给模型模型判断是否需要调用工具、调用哪个、参数是什么。执行工具你的程序拿到模型返回的工具名和参数去实际执行这个函数拿到结果。结果回传你把工具执行的结果再发给模型模型基于结果生成最终回答。整个过程中模型从来没有真正“执行”过任何东西。它只是输出了一个结构化的调用请求真正干活的是你的代码。这个设计非常巧妙因为它在赋予模型“手脚”的同时保证了安全性和可控性——你完全可以决定哪些工具可以被调用、参数怎么校验、执行结果怎么处理。3.2 工具定义的几个关键细节工具定义的质量直接决定了调用的准确率。我总结了几条经验工具描述要写“什么时候用”而不是“这是什么”。很多人写工具描述就写“查询天气的函数”这个信息量太少了。更好的写法是“当用户询问某个城市的天气、温度、是否下雨等问题时使用此工具”。模型需要知道的是使用场景而不是函数的技术实现。参数描述要具体到格式。比如日期参数你要写“格式为YYYY-MM-DD例如2024-01-15”而不是只写“日期”。模型对格式的推断能力有限你给得越明确它填的参数越准确。工具数量不要太多。我试过一次性给模型挂二十多个工具结果调用准确率明显下降。模型在太多选项面前会犹豫、会选错。我的经验是单次对话暴露的工具不超过十个如果确实有很多功能可以分组或者做路由。参数尽量用枚举。如果一个参数只有几个固定取值用enum列出来比让模型自由发挥要可靠得多。3.3 多工具编排的实战思路实际项目里用户的一个需求往往需要多个工具配合完成。比如“帮我查一下这周北京的天气然后推荐每天穿什么”这就需要先调天气工具再根据天气数据做推理可能还要调一个服装推荐工具。这种多工具编排有两种做法。一种是让模型自己决定调用顺序你只需要把所有工具都给它它会在多轮对话中逐步调用。另一种是用代码编排固定流程模型只负责在每一步做决策。前者灵活但不可控后者可控但不够灵活。我的建议是流程固定的场景用代码编排流程不固定的场景让模型自主决策。比如客服机器人用户可能问天气、问订单、问退换货这些流程之间没有固定顺序就让模型自己判断。但如果是“根据天气推荐穿搭”这种固定流程直接用代码串起来更稳定。4. 从零搭建一个工具调用实战案例4.1 场景定义与技术选型我拿一个实际做过的项目来拆解智能差旅助手。用户输入“我下周三要去上海出差三天帮我看看天气和需要带什么”系统需要完成查天气、分析天气数据、给出穿衣和携带物品建议。技术选型上模型我用的是支持工具调用能力的通用大模型开发框架用Python工具执行层用普通的函数。为什么不直接用LangChain这类框架因为我想把底层链路讲清楚用原生方式实现一遍你理解了原理之后再用框架就是水到渠成的事。4.2 工具定义与参数设计先定义两个工具。第一个是天气查询工具weather_tool { type: function, function: { name: get_weather, description: 查询指定城市指定日期的天气信息包括温度、天气状况、风力。当用户询问天气相关问题时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 }, date: { type: string, description: 日期格式为YYYY-MM-DD例如2024-06-15 } }, required: [city, date] } } }第二个是穿衣建议工具clothing_tool { type: function, function: { name: get_clothing_advice, description: 根据温度和天气状况给出穿衣和携带物品建议。当需要根据天气推荐穿搭时使用。, parameters: { type: object, properties: { temperature: { type: number, description: 温度摄氏度 }, condition: { type: string, description: 天气状况, enum: [晴, 多云, 阴, 小雨, 中雨, 大雨, 雪] }, days: { type: integer, description: 出差或出行天数 } }, required: [temperature, condition, days] } } }注意几个细节天气工具的描述里写了“当用户询问天气相关问题时使用”这是使用场景日期参数给了格式示例穿衣工具的条件参数用了enum限定取值范围。这些细节看起来不起眼但对调用准确率的影响很大。4.3 完整调用流程的代码实现接下来是核心的调用循环。整个逻辑是一个while循环发请求→检查是否有工具调用→执行工具→把结果加回消息列表→继续请求直到模型不再调用工具为止。import json def run_conversation(user_input, tools, tool_functions, max_turns5): messages [ {role: system, content: 你是一个差旅助手根据用户需求调用工具获取信息然后给出建议。}, {role: user, content: user_input} ] for turn in range(max_turns): response call_llm(messages, toolstools) msg response.choices[0].message # 没有工具调用直接返回最终回答 if not msg.tool_calls: return msg.content # 把模型的工具调用请求加入消息历史 messages.append(msg) # 逐个执行工具调用 for tool_call in msg.tool_calls: func_name tool_call.function.name func_args json.loads(tool_call.function.arguments) # 执行对应的函数 result tool_functions[func_name](**func_args) # 把结果加入消息历史 messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) return 达到最大轮次限制这个循环里有个关键点每次工具执行的结果都要以tool角色加回消息列表并且带上tool_call_id。这个id是用来和模型的调用请求做对应的如果对不上模型会报错或者忽略结果。4.4 参数校验与异常处理实际跑起来之后你会发现模型传过来的参数不一定完全符合预期。比如日期格式可能写成“2024/06/15”而不是“2024-06-15”城市名可能带“市”字也可能不带。所以工具函数内部一定要做参数校验和容错。from datetime import datetime def get_weather(city, date): # 日期格式容错 date date.replace(/, -).replace(., -) try: datetime.strptime(date, %Y-%m-%d) except ValueError: return {error: f日期格式不正确: {date}请使用YYYY-MM-DD格式} # 城市名容错 city city.replace(市, ).strip() # 实际查询逻辑这里用模拟数据 # ... return {city: city, date: date, temperature: 28, condition: 多云, wind: 3级}异常处理的原则是工具内部不要抛异常而是返回结构化的错误信息。因为异常会中断整个流程而错误信息会被模型看到它可以根据错误信息调整参数重新调用。比如日期格式错了模型看到错误提示后会自动修正格式再试一次。5. 踩坑实录与排查手册5.1 工具调用中最容易翻车的几个点模型不调用工具直接编答案。这是最常见的问题。用户问天气模型不调工具直接编一个“明天晴25度”。解决方案有两个一是在系统提示词里明确要求“所有天气相关问题必须调用get_weather工具获取数据不得自行编造”二是如果检测到模型没有调用工具就回答了事实性问题可以在代码层面强制拦截重新请求。参数类型不匹配。模型传过来的温度是字符串“28”而不是数字28或者天数传了“三天”而不是3。解决方案是在工具函数入口做类型转换和校验同时把参数描述写得更明确。多轮调用时上下文丢失。模型在第一轮调了天气工具第二轮需要根据天气结果调穿衣工具但它忘了第一轮的结果。这是因为工具返回的结果没有被正确加入消息历史。检查你的消息列表确保每次工具结果都带上了正确的tool_call_id。并行调用时结果错位。模型可能一次性返回多个工具调用请求如果你串行执行但结果顺序和请求顺序不一致就会错位。解决方案是严格按照tool_call_id来匹配而不是靠顺序。5.2 常见问题速查表问题现象可能原因排查方向解决方案模型不调用工具工具描述不清晰检查description是否说明了使用场景补充使用场景描述系统提示词中强制要求调用参数错误参数描述不具体检查参数description和类型定义补充格式示例使用enum限定取值调用结果被忽略消息历史格式错误检查tool角色消息是否带tool_call_id确保每条工具结果都对应正确的id多轮调用死循环模型反复调用同一工具检查工具返回结果是否包含模型需要的信息设置最大轮次限制优化工具返回值工具选择错误工具数量过多或描述重叠检查工具之间功能是否有交叉精简工具列表明确各工具边界5.3 几个提升稳定性的实战技巧给工具调用加超时和重试。外部API可能超时你的工具函数要有超时机制超时后返回一个错误信息让模型决定是重试还是换方案。记录完整的调用日志。每次工具调用的输入参数、输出结果、耗时都记下来。出问题的时候这些日志是你排查的唯一依据。用少量示例引导工具调用。在系统提示词里给一两个“用户问→调用什么工具→参数怎么填”的示例能显著提升首次调用的准确率。工具返回值要精简。不要把整个API返回的JSON原封不动塞回去只提取模型需要的字段。返回值越大模型处理越慢也越容易忽略关键信息。6. 从单工具到多工具编排的进阶思路6.1 工具分组与路由策略当工具数量超过十个之后一次性全部暴露给模型会导致选择困难。我的做法是做一层路由先用一个轻量级的分类模型或者规则引擎判断用户意图属于哪个领域然后只把该领域相关的工具暴露给模型。比如差旅助手可以分成“天气组”、“交通组”、“酒店组”、“报销组”用户问天气就只加载天气组的工具。这样每次模型面对的选项不超过五个调用准确率会高很多。6.2 工具之间的依赖处理有些工具之间存在依赖关系。比如查酒店需要先知道入住日期而入住日期可能来自另一个工具的输出。这种依赖关系有两种处理方式一种是在工具描述里写清楚依赖条件让模型自己判断调用顺序另一种是在代码层面做编排把有依赖关系的工具串成工作流。我倾向于简单依赖用模型自主决策复杂依赖用代码编排。判断标准是如果依赖关系可以用一两句话描述清楚模型能处理如果需要多步条件判断和循环代码更可靠。6.3 错误恢复与降级方案工具调用不可能百分之百成功。网络超时、API限流、参数错误都会发生。好的系统设计要考虑降级方案天气API挂了能不能用缓存数据酒店查询超时了能不能给用户一个预估价格我的做法是每个工具都配一个降级函数主函数失败时自动调用降级函数返回兜底数据同时在结果里标注“此为预估数据”。这样用户体验不会因为某个外部服务不可用而完全中断。7. 提示词与工具调用的配合心法7.1 系统提示词里要写清楚工具使用规则工具调用的行为很大程度上受系统提示词影响。我通常会在系统提示词里写这几条规则所有事实性信息必须通过工具获取不得自行编造调用工具前先确认参数是否完整缺少必要参数时先向用户询问工具返回错误时根据错误信息调整参数重试最多重试两次多个工具可以并行调用时尽量并行减少响应时间这些规则不需要很长但每一条都能显著改善调用行为。7.2 用户提示词要引导而非命令用户提示词的作用是提供任务上下文而不是重复系统提示词里的规则。比如用户说“下周三去上海”你不需要在用户提示词里再写一遍“请调用天气工具”模型看到系统提示词里的规则和工具列表自己会判断。7.3 多轮对话中的状态管理多轮对话里工具调用的结果需要被后续轮次引用。比如第一轮查了天气第二轮用户问“那需要带伞吗”模型需要知道第一轮的天气结果。这就要求你把工具调用结果保留在消息历史里而不是每轮清空。但消息历史也不能无限增长否则token消耗会失控。我的做法是保留最近五轮的工具调用结果更早的做摘要压缩。8. 我个人的一些经验体会做了一段时间的工具调用项目之后我最大的感受是提示词和工具调用不是两个独立的东西而是一体两面。提示词决定了模型“怎么想”工具调用决定了模型“怎么做”。两者配合好了模型才能真正从“会聊天”变成“会干活”。另一个体会是不要追求一步到位。我见过很多项目一上来就想做全自动的智能体结果各种边界情况处理不过来。更务实的做法是从单工具、单场景开始跑通了再逐步增加工具和场景。每增加一个工具都要重新测试调用准确率和异常处理。还有一个容易被忽略的点是工具返回结果的可读性。模型不是人它对JSON的解析能力有限。如果工具返回的是一大坨嵌套JSON模型很容易看漏关键字段。我通常会把工具结果整理成扁平的结构关键信息放在最前面必要时加一个自然语言的摘要字段。最后说一个关于提示词的心得好的提示词是改出来的不是写出来的。第一版提示词能跑通60%的场景就不错了剩下的40%需要你不断观察失败案例分析是提示词的问题还是工具的问题然后针对性调整。这个过程没有捷径但每改一版你都会对模型的行为有更深的理解。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进