三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

面向LLM编程:从意图解析到工具调用的工程实践指南

面向LLM编程:从意图解析到工具调用的工程实践指南

1. 先搞清楚“面向LLM编程”到底在解决什么实际问题

如果你最近在接触大语言模型(LLM)相关的开发,无论是想做个智能客服、文档问答,还是想用AI辅助写代码,大概率都听过“面向LLM编程”这个词。听起来很玄乎,好像是一种全新的编程范式。但根据我自己的实践和观察,它最核心的价值,其实是解决一个非常具体的问题:如何把一个模糊、不确定的自然语言用户请求,稳定、可靠地转换成计算机能执行的、结构化的任务流程。

这和我们传统的编程思维完全不同。传统编程是“确定性”的:你写if-else,输入A就必然得到B。但LLM是“概率性”的:你问它“今天天气怎么样”,它可能直接回答天气,也可能先问你“你在哪个城市?”。这种不确定性,让直接把LLM当做一个函数来调用变得非常脆弱。

所以,“面向LLM编程”不是让你去学一门新语法,而是让你转变设计思路。它的核心是把LLM看成一个具有统计特性的“组件”,你需要围绕这个组件的特性(比如擅长理解、不擅长精确计算、输出有随机性)来构建整个系统。这就像你用电机(特性是旋转)去设计一辆车,和你用内燃机(特性是往复运动)去设计,架构肯定不一样。

这篇文章,我就结合一些常见的开发场景,拆解一下这种编程思路里几个关键的“统计组件”该怎么用,以及在实际落地时,从环境准备到任务编排,再到错误处理,每一步需要注意什么。无论你是想快速验证一个AI点子,还是打算构建一个更稳定的生产级应用,下面的内容都能帮你避开初期最容易踩的那些坑。

2. 核心组件拆解:任务不是直接问,而是拆成链

很多人第一次用LLM API,习惯就是“用户问什么,我就直接把问题扔给模型,然后返回结果”。这在Demo阶段没问题,但一旦上点复杂度,比如用户问“帮我分析一下上周的销售数据,并总结成一份报告”,这种简单调用模式立刻就会崩盘。输出可能格式混乱,可能遗漏关键点,甚至可能胡言乱语。

面向LLM编程,首先就要打破这种“一问一答”的思维。我们需要引入几个核心的、具有不同统计功能的组件,把它们像流水线一样组装起来。

2.1 组件一:意图解析与槽位填充

这是整个流程的“调度中心”。它的任务不是直接生成最终答案,而是把用户的自然语言指令,解析成一个结构化的任务描述。

它具体做什么?

  1. 识别意图:判断用户想干什么。是“问答”、“总结”、“翻译”、“写代码”还是“数据分析”?
  2. 提取参数:从指令里提取关键信息。比如“上周的销售数据”里,“上周”是时间参数,“销售数据”是数据实体。

为什么需要它?因为LLM在长上下文里同时做理解和执行,效果会下降。先让一个专门的“解析器”LLM(或用规则+小模型)把任务结构化,后面的步骤就清晰了。这相当于把“非结构化输入”变成了“结构化查询”。

实操建议:

  • 不要追求一次完美解析:可以设计成交互式。例如,如果解析器发现“时间”参数不明确,它可以生成一个澄清问题:“您指的是具体哪一天,还是过去7天?”,让用户确认,再把确认后的结构化任务传给下游。
  • 输出必须结构化:强制要求解析器以JSON格式输出。例如:{"intent": “generate_report”, “parameters”: {"time_range": “last_week”, “data_source”: “sales”}}。这为后续的自动化处理打下了基础。

2.2 组件二:上下文检索与增强

当任务明确后,LLM往往需要额外的知识来回答,比如公司内部的文档、最新的产品信息、历史上的对话记录。直接把这些海量文本全塞进LLM的上下文窗口,既不经济(更贵更慢),效果也差(关键信息被淹没)。

这就是RAG的核心价值。它作为一个独立组件,负责:

  1. 检索:根据解析后的任务参数(如“销售数据”),从向量数据库或知识库中,找出最相关的几段文本。
  2. 增强:把这些检索到的文本片段,作为“参考材料”和用户的原始问题一起,重新组合成一个新的、信息更丰富的提示,交给LLM去生成最终答案。

为什么需要它?它解决了LLM的“静态知识”和“幻觉”问题。让LLM的答案基于你提供的事实依据,而不是它内部可能过时或虚构的记忆。

实操建议:

  • 检索质量是关键:检索器(通常是嵌入模型)的效果直接决定最终答案的上限。如果检索不到相关文档,LLM编得再像也没用。
  • 给检索结果加“引用”:在构造给LLM的提示时,明确标注哪段话来自哪个文档。甚至可以要求LLM在生成答案时,注明依据的源文档编号。这对生产环境追溯答案来源至关重要。

2.3 组件三:思维链与任务分解

对于复杂任务,即使有了上下文,直接让LLM生成最终答案也可能导致逻辑混乱。这时需要引入“规划”组件。

它负责把一个大任务,拆解成一系列顺序或并行的子任务。比如“分析销售数据并生成报告”可以拆解为:

  1. 子任务A:从数据库获取上周销售数据。
  2. 子任务B:计算关键指标(总额、环比、最佳商品)。
  3. 子任务C:根据指标,用文字描述分析结论。
  4. 子任务D:将结论和指标组织成报告格式。

为什么需要它?

  • 降低单步复杂度:每个子任务对LLM来说都更简单、更专注,出错率更低。
  • 便于插入确定性工具:在子任务中,可以灵活地调用确定性工具。比如,子任务A和B完全可以不用LLM,而是用SQL查询和Python计算,这样精度是100%。LLM只负责它擅长的C和D(自然语言组织和撰写)。
  • 实现更复杂的逻辑:比如循环(“直到用户满意为止”)、条件判断(“如果指标为负,则执行预警子任务”)。

2.4 组件四:工具调用

这是让LLM从“聊天脑”变成“实干家”的关键。LLM本身不会执行操作,但它可以学习在何时、以何种参数去调用一个工具(函数)。

工具是什么?任何确定性的函数都可以是工具:执行一段代码、查询数据库、调用外部API、操作文件、点击按钮。工具调用的流程

  1. 向LLM描述可用的工具列表(函数名、功能、参数格式)。
  2. LLM根据当前任务和对话历史,决定是否需要调用工具,以及调用哪个工具,并生成符合格式的参数。
  3. 系统执行该工具函数,获得确定性的结果。
  4. 将工具执行结果作为新的上下文,再返回给LLM,让它基于结果继续生成回复。

为什么需要它?它弥补了LLM在精确性、实时性和执行能力上的不足。LLM负责“想”,工具负责“做”。

实操建议:

  • 工具描述要清晰:给LLM的工具说明必须无歧义,参数类型、格式要明确。模糊的描述会导致错误的调用。
  • 做好错误处理:工具执行可能失败(网络超时、参数错误)。要在流程中设计好:当工具调用失败时,是让LLM重试、选择其他工具,还是直接向用户报错。

3. 从零搭建:环境、框架与第一个链

理解了核心组件,我们来看如何动手搭一个最简单的链。这里不推荐一上来就追求大而全的框架,先从最小可行产品开始。

3.1 环境与依赖准备

你需要准备两样东西:LLM的接入能力,和一个能帮你编排组件的框架。

1. LLM API密钥:

  • 国内可选:智谱AI、百度文心、阿里通义、月之暗面等。访问其开放平台,注册并获取API Key。注意查看计费方式和速率限制。
  • 备用方案:也可以使用开源的LLM模型在本地部署,如ChatGLM3、Qwen等,但这需要一定的GPU资源和技术精力。对于快速验证,建议先从云API开始。
  • 关键点:拿到Key后,先别急着写代码。用curl或Postman调一下最简单的对话接口,确认网络连通、鉴权成功、返回正常。这能排除掉后续90%的基础环境问题。

2. 编程框架选择:

  • LangChain:生态最丰富,组件齐全,文档多。但抽象层次较高,新手可能觉得“黑盒”太多。
  • LlamaIndex:专注于RAG场景,在数据连接和检索方面非常强大。
  • Semantic Kernel:微软出品,与.NET生态结合好,强调规划和插件(工具)。
  • 简单自制:对于理解原理,完全可以不用框架,就用requests库调用API,用Python字典和列表来管理对话历史。这能让你对流程有绝对控制力。

我的建议:初学者可以从LangChain开始,因为它提供了现成的组件,能让你快速看到效果。但一定要同时看它的源码或简化示例,理解每个组件背后在做什么。

3.2 构建你的第一个智能链:一个天气查询助手

我们用一个经典例子:让AI帮你查天气。这需要用到意图解析工具调用

步骤1:定义工具我们先定义一个确定性的工具函数,它模拟调用一个天气API。

import requests def get_weather(city: str) -> str: """根据城市名查询天气信息。 Args: city: 城市名称,例如“北京”、“Shanghai”。 Returns: 返回该城市的天气情况字符串。 """ # 这里为了演示,我们模拟一个返回。真实情况应调用如心知天气、和风天气等API。 weather_data = { "北京": "北京:晴,15~25℃,微风。", "上海": "上海:多云,18~27℃,东南风3级。", "广州": "广州:雷阵雨,25~32℃,南风4级。" } return weather_data.get(city, f"抱歉,未找到{city}的天气信息。")

步骤2:构建提示,让LLM学会调用工具我们需要告诉LLM,有这个工具可用,并规定它思考的格式。这里我们使用一种简单的“思维-行动”格式。

# 系统提示,设定AI的角色和规则 system_prompt = """ 你是一个有帮助的助手,可以调用工具来获取信息。 你可以使用的工具是: - get_weather(city: str): 查询指定城市的天气。 请遵循以下格式响应用户: 用户:用户的问题 思考:你需要先思考是否需要调用工具,以及调用哪个工具。 行动:如果需要调用工具,则输出“工具调用: get_weather(城市名)”。如果不需要,则输出“行动: 无”。 结果:工具返回的结果,或者你的直接回答。 最终答案:根据以上所有信息,给用户的最终回复。 """ # 用户问题 user_query = “今天北京天气怎么样?”

步骤3:组装并执行流程我们将系统提示、用户问题、历史对话(这里为空)组合成一个完整的提示,发送给LLM。

# 假设你已经有了调用LLM API的函数 call_llm def call_llm(messages): # 这里简化处理,实际需替换为真实的API调用,如OpenAI, ZhiPu等 # 模拟LLM的回复 # 一个“聪明”的LLM应该能根据我们的提示格式进行回复 return """ 思考:用户询问北京天气,我需要调用get_weather工具。 行动:工具调用: get_weather(北京) 结果:等待工具返回... """ # 第一次调用LLM,让它决定行动 messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_query} ] llm_response_1 = call_llm(messages) # 解析出LLM想要执行的动作 # 这里需要简单的文本解析来提取“get_weather(北京)” import re action_match = re.search(r'工具调用:\s*get_weather\((\w+)\)', llm_response_1) if action_match: city = action_match.group(1) # 执行工具 tool_result = get_weather(city) # 将工具结果作为新的上下文,再次发送给LLM,让它生成最终答案 messages.append({"role": "assistant", "content": llm_response_1}) messages.append({"role": "user", "content": f"工具执行结果:{tool_result}"}) llm_response_final = call_llm(messages) print(llm_response_final) else: # 如果LLM认为不需要工具,直接输出它的回答 print(llm_response_1)

步骤4:解析最终输出理想情况下,LLM的第二次回复会包含“最终答案:北京:晴,15~25℃,微风。”。

这个简单的链条包含了面向LLM编程的核心思想:LLM负责决策(要不要调用工具,参数是什么),系统负责执行确定性的逻辑(调用API函数),两者协作完成复杂任务。

4. 进阶实践:处理复杂任务与生产化考量

当你把单条链跑通后,接下来就要考虑更复杂的任务和如何让它更稳定,适合实际使用。

4.1 构建一个多步骤的智能分析链

假设我们要做一个“行业新闻分析助手”:用户输入一个公司名,自动抓取近期新闻,并分析舆论情感。

这个链会包含更多组件:

  1. 意图解析:识别出是“新闻分析”意图,提取公司名。
  2. 工具调用1:调用新闻爬虫工具(或API),获取关于该公司的最新新闻列表和摘要。
  3. 思维链规划:LLM判断新闻数量,决定是逐条分析还是总结分析。
  4. 工具调用2:对每条新闻或总结文本,调用情感分析工具(可以是另一个LLM,也可以是专门的NLP模型)。
  5. 结果汇总:LLM综合所有新闻的情感分析结果,生成一份简要报告(正面/负面/中性占比,主要关注点)。

这里的关键是状态管理。每个步骤都会产生输出,这个输出是下一个步骤的输入。你需要设计一个数据结构(比如一个WorkflowState字典)来承载整个流程的状态,包括原始输入、中间结果、最终输出等。

4.2 生产环境必须考虑的要点

Demo能跑只是第一步,要让服务可靠,必须处理以下问题:

1. 稳定性与错误处理:

  • LLM API调用失败:网络超时、服务限流、token超限。必须有重试机制(如指数退避)和熔断降级(失败次数过多则暂时禁用该功能或切换备用模型)。
  • 工具调用失败:同上,需要有重试和超时设置。
  • LLM输出格式不符合预期:这是最常见的问题。你的代码不能假设LLM永远会严格按照你规定的“行动:...”格式输出。必须有输出解析和格式校验。例如,使用Pydantic库定义你期望的输出结构,并让LLM以JSON格式输出,然后在代码中尝试解析。解析失败时,可以尝试修复(如让LLM重试),或转入人工处理流程。

2. 性能与成本:

  • 缓存:对于相同或相似的查询(例如,不同用户都问“苹果公司的最新新闻”),结果可以缓存一段时间,避免重复调用昂贵的LLM和工具API。
  • 异步处理:对于耗时长(如分析大量文档)的任务,应该设计为异步。用户发起请求后立即返回一个“任务ID”,后端处理完成后,通过WebSocket、轮询或回调通知用户。
  • Token管理:LLM按Token收费。要优化提示词,减少不必要的上下文。在RAG场景下,精心设计检索策略,只返回最相关的片段,而不是全部文档。

3. 可观测性与评估:

  • 全链路日志:记录每个环节的输入、输出、耗时、Token使用量、工具调用详情。这是排查问题的唯一依据。
  • 评估体系:如何知道你的AI应用做得好不好?需要定义评估指标。对于问答系统,可以是“答案相关性”、“事实准确性”。可以通过人工抽检、或用更强大的LLM(如GPT-4)作为裁判来自动评分。没有评估,优化就无从谈起。

4. 安全与合规:

  • 输入过滤:防止用户输入恶意提示词进行“提示注入攻击”,诱导AI执行不当操作或泄露系统提示。
  • 输出过滤:对AI生成的内容进行审核,过滤不当、偏见或有害信息。
  • 数据隐私:确保用户上传的文档、对话记录等数据得到妥善保护,符合相关法律法规。

5. 常见陷阱与调试心法

即使理解了所有概念,实际开发中还是会遇到各种奇怪问题。下面是我总结的几个高频陷阱和排查思路。

5.1 陷阱一:把LLM当数据库用

现象:问一个非常具体、需要精确记忆的知识,比如“我们公司Q3的营收具体数字是多少?”,LLM开始胡编乱造。根因:LLM是语言模型,不是数据库。它的“知识”是训练数据中统计规律的体现,无法保证精确回忆。解决:这类问题必须靠RAG。把公司财报、数据库等精准信息存入向量库,让LLM去检索并基于检索到的片段回答。

5.2 陷阱二:提示词过于复杂或模糊

现象:AI的表现不稳定,有时很好,有时完全跑偏。根因:提示词写得像散文,充满了“请尽可能”、“努力地”、“生成一份优秀的”这种模糊词汇。LLM无法理解你的潜台词。解决

  • 结构化:使用清晰的标记,如“### 指令 ###”、“### 示例 ###”、“### 格式要求 ###”。
  • 具体化:把“优秀”拆解成可衡量的标准,如“报告需包含:1. 数据概述;2. 三个关键发现;3. 一项行动建议”。
  • 提供示例:给出一两个输入输出的例子,让LLM明确知道你想要什么格式和风格。

5.3 陷阱三:忽略上下文窗口限制

现象:在处理长文档或多轮对话后,AI似乎“失忆”了,忘记了对话开头的内容。根因:所有LLM都有上下文长度限制(如4K、8K、32K tokens)。超出部分会被丢弃。解决

  • 摘要压缩:在长对话中,定期将历史对话总结成一个简短的摘要,用摘要代替原始长文作为新的上下文。
  • 选择性记忆:只将与当前任务最相关的历史片段放入上下文。
  • 外部记忆体:将重要的历史信息存入数据库,当需要时再通过检索方式引入。

5.4 调试心法:从简到繁,逐层隔离

当你的链不工作时,不要一头扎进代码里。按顺序排查:

  1. 单组件测试:你的get_weather工具函数,单独给它输入“北京”,能返回正确结果吗?你的LLM API调用,用最简单的对话提示,能正常回复吗?
  2. 提示词测试:把你精心设计的、复杂的提示词,直接复制到ChatGPT网页版(或你所用的模型的官方Playground)里,用几个典型输入测试,看输出是否符合预期?如果在这里就不行,那问题就在提示词本身。
  3. 简化链测试:先搭建一个只有2个组件的超简链(如:用户输入 -> LLM解析意图 -> 打印解析结果)。这个能跑通吗?
  4. 查看完整日志:打开DEBUG级别的日志,查看LLM接收到的完整提示词是什么,它返回的完整响应是什么。很多时候问题就出在字符串拼接时多了个空格、少了换行符。
  5. 模拟LLM响应:在调试时,可以暂时“Mock” LLM的响应,让它固定返回一个你预设的、正确的响应。如果这样链能走通,说明问题出在LLM的实际响应不符合你的解析逻辑。你需要调整提示词或加强输出解析的鲁棒性。

面向LLM编程,本质上是一种新的软件工程实践。它要求开发者同时具备软件架构的严谨性和对概率模型特性的深刻理解。最有效的学习方式不是死记硬背框架API,而是亲手搭建一个最简单的链,然后不断地增加复杂度,在解决一个又一个具体问题的过程中,去体会这些“统计组件”如何协同工作。记住,你的目标是构建一个可靠的系统,而LLM只是这个系统中一个强大但需要精心管理的组件。

← 返回列表