ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

大模型应用开发实战:从对话到RAG与Agent

大模型应用开发实战:从对话到RAG与Agent 如果你最近关注过大模型相关的科技新闻可能看到过这样一条略带幽默的消息旧金山某处广告牌上出现了 “ChatTJB” 的推广宣传标语很有意思大意是 “Human-powered LLM”——背后不是 GPU 集群而是一群真人在聊天框后面模拟大模型回复。这显然是一个带着讽刺意味的 parody当 AI 热潮席卷一切时到底有多少产品真的在用 LLM又有多少只是在用人工流程“假装”智能抛开营销段子不谈这块广告牌其实点出了一个非常核心的问题我们在谈论 LLM 应用开发时究竟在谈论什么是 API 调用、Prompt 工程还是 RAG、Agent、MCP、模型精度这些工程问题这篇文章想借这个由头从概念到代码完整梳理一条适合后端开发者和算法工程师上手的 LLM 应用实战路径。如果你正在准备做一个 AI 客服、知识库问答系统或者想让大模型自己去调用工具完成复杂任务这篇文章会很有帮助。1. 从一块广告牌说起LLM 到底是什么1.1 “人类供电的 LLM” 讽刺了什么ChatTJB 的广告之所以让人会心一笑是因为它戳破了一个行业潜台词在很多团队里自然语言交互界面背后的“智能”未必来自大模型也可能来自人工客服、脚本模板、规则引擎甚至是外包人力。这种 “Human-in-the-loop” 的做法并不丢人很多业务流程确实需要让人工介入兜底但它和真正意义上的 LLM 应用有两个本质区别扩展性不同真人服务无法像模型推理一样线性扩展遇到流量高峰时只能排队。知识容量不同模型经过海量语料预训练具备开放领域的知识泛化能力人工规则很难覆盖长尾问题。广告牌用 “human-powered LLM” 这个说法本质上是在调侃一种现象只要聊天框做得像 LLM用户就会默认背后有一套大语言模型。这提醒我们在开发 LLM 应用时不要只关心界面是否“像 AI”更要关心模型、知识、工具和工程链路是否真的打通。1.2 LLM 应用开发的核心问题真正的 LLM 应用开发通常需要回答这几个问题模型从哪里来直接调用商业 API还是部署开源模型知识从哪来模型没学过的内部文档、实时数据如何补充给它能力边界在哪模型不会查数据库、不会操作业务系统如何让它具备工具调用能力成本和延迟如何控制同样一个需求是否可以换更小的模型或者用更少的 Token 完成这些问题的答案构成了 RAG、Agent、MCP 等概念出现的背景。理解这些概念不是让你记住名词而是为了在做技术选型时知道什么时候该用什么。1.3 本文的实战目标读完这篇文章你会完成三个层次的动手实践用 HTTP 请求调用一个 OpenAI 兼容的 LLM 接口完成最小对话程序在不依赖重型框架的前提下实现一个极简 RAG 知识库问答流程通过 Function Calling 让模型调用外部工具并理解 MCP 在其中的作用。同时我会把高频报错、精度选择、Prompt 管理、成本控制这些工程问题整理成可以直接参考的清单。你会发现LLM 应用开发并没有玄学它只是把“模型”和“工程”放在一起思考。2. 环境准备与基础概念2.1 运行环境与依赖本文示例以 Python 为例建议使用 Python 3.10 或更高版本。你不需要一开始就安装一大堆框架核心依赖只有三个requests用于调用 HTTP APInumpy用于向量计算pandas可选用于演示文档数据清洗。如果你需要本地向量检索可以再安装chromadb或faiss-cpu如果不想安装向量数据库我会在示例中直接用 numpy 实现相似度检索。这种做法的优点是依赖少、逻辑清晰适合学习原理。创建一个项目目录mkdir llm-practice cd llm-practice python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install requests numpy如果你的公司内部已经部署了兼容 OpenAI 协议的模型服务可以直接复用下面的接口地址。如果使用云端 API就把BASE_URL和API_KEY换成自己的配置。2.2 必备概念Token、上下文窗口、Prompt在写代码之前先理解三个最基础的概念。Token是模型处理文本的最小单位。一个 Token 不一定等于一个汉字或一个英文单词它可能是半个词也可能是几个字符。调用模型时输入和输出都会消耗 Token所以 Token 既是质量指标也是成本指标。上下文窗口是模型一次能看到的文本总量。比如某个模型的上下文窗口是 8K意味着输入和输出加起来不能超过约 8000 个 Token。超过后要么截断文本要么对历史消息做压缩摘要否则接口会报错。Prompt是你发给模型的指令。它可以是简单的问题也可以是一段包含背景信息、输出格式、例子、约束条件的系统提示词。Prompt 写得好不好直接决定模型输出质量。这三者的关系可以这么理解Token 是“字数”上下文窗口是“能看到的纸面大小”Prompt 是“你在纸上写下的任务说明”。2.3 模型部署方式API 与本地推理选择模型服务时通常有两种方式。第一种是使用外部 API。优点是接入快、无需 GPU、模型版本迭代由服务商维护缺点是数据出网、单次调用有成本、可用性依赖供应商。第二种是在内网部署开源模型比如 llama.cpp、vLLM、Ollama 等推理引擎。优点是数据可控、边际成本低缺点是需要 GPU 资源还要处理模型精度、显存、并发等问题。无论哪种方式最好让应用层统一走 OpenAI 兼容协议。这样上层代码不用变只需要切换BASE_URL和MODEL_NAME就能在不同模型服务之间迁移。这也是本文代码示例采用 HTTP 接口而不是绑定某个 SDK 的原因。3. 最小可用写一个 LLM 对话程序3.1 创建项目结构在项目目录下创建配置文件config.py把接口地址和密钥统一管理# 文件路径llm-practice/config.py import os # 从环境变量读取避免把密钥写死在代码里 BASE_URL os.getenv(LLM_BASE_URL, https://api.openai.com/v1) API_KEY os.getenv(LLM_API_KEY, your-api-key) MODEL_NAME os.getenv(LLM_MODEL_NAME, gpt-4o-mini)注意这里默认使用的是 OpenAI 兼容格式。如果你用的是本地 OllamaBASE_URL可以填http://localhost:11434/v1模型名填如llama3.1同样能跑通。3.2 使用 requests 调用兼容 API在chat.py中写一个最简单的对话函数# 文件路径llm-practice/chat.py import requests import config def chat(messages, temperature0.7): url f{config.BASE_URL}/chat/completions headers { Authorization: fBearer {config.API_KEY}, Content-Type: application/json, } payload { model: config.MODEL_NAME, messages: messages, temperature: temperature, stream: False, } resp requests.post(url, headersheaders, jsonpayload, timeout120) resp.raise_for_status() data resp.json() return data[choices][0][message][content]这段代码做了三件事构造请求头、发送messages列表到/chat/completions接口、解析返回的choices数组。messages是对话消息列表通常包含system系统指令、user用户输入、assistant模型回复这种结构保证了多轮对话的上下文连续性。3.3 流式输出与非流式输出上面的例子是stream: False模型会一次性返回完整结果。对聊天体验更友好的方式是流式输出让文字像打字机一样逐步显示。流式接口返回的是text/event-stream格式需要逐行解析# 文件路径llm-practice/chat_stream.py import requests import json import config def chat_stream(messages): url f{config.BASE_URL}/chat/completions headers { Authorization: fBearer {config.API_KEY}, Content-Type: application/json, } payload { model: config.MODEL_NAME, messages: messages, stream: True, } resp requests.post(url, headersheaders, jsonpayload, timeout120, streamTrue) resp.raise_for_status() content for line in resp.iter_lines(): if not line: continue line line.decode(utf-8) if line.startswith(data: ): data_str line[6:] if data_str.strip() [DONE]: break data json.loads(data_str) delta data[choices][0][delta].get(content) if delta: content delta print(delta, end, flushTrue) return content流式输出对服务端和客户端都更友好但调试时也更容易碰到连接中断、解析失败的问题。入门阶段建议先掌握非流式调用再升级为流式。3.4 运行与验证在入口文件main.py中组合消息并调用# 文件路径llm-practice/main.py from chat import chat messages [ {role: system, content: 你是一个简洁、专业的技术助手。}, {role: user, content: 请用三句话解释什么是大语言模型。}, ] result chat(messages) print(result)运行后你可能会看到类似下面的输出大语言模型是基于海量文本数据训练得到的神经网络模型它通过预测文本序列来学习语言规律。模型能够理解自然语言指令并根据上下文生成合理回复。它在对话、写作、代码生成等任务中表现出色。到这一步你已经拥有一个最小可用的 LLM 对话程序。接下来要做的是让模型具备“读你的文档”和“调用你的工具”的能力。4. 进阶实战RAG 知识库问答4.1 为什么直接问模型不够大模型的知识来自训练数据它不知道你公司内部的最新制度、某套私有 API 的文档也不会知道 2025 年之后发生的事件。想让模型回答这类问题常见做法有微调Fine-tuning和检索增强生成RAG。微调是修改模型权重成本高、周期长RAG 则是在不修改模型的前提下先从外部知识库检索相关内容再把这些内容拼进 Prompt让模型基于材料回答。RAG 的优势在于知识更新只需要重新索引文档不需要重新训练模型引用了具体来源降低了模型“一本正经地胡说八道”的概率。4.2 RAG 的四个步骤一个完整的 RAG 流程可以拆成四步文档加载与切块把 PDF、Word、Markdown 等文档读进来按固定长度切分成多个 chunk向量化用 Embedding 模型把每个 chunk 编码成向量检索用户提问后把问题也编码成向量在向量库中计算相似度召回 Top K 个 chunk增强生成把召回的 chunk 和用户问题一起组装成 Prompt交给大模型生成回答。下面用最少的依赖实现一个极简 RAG主要用 HTTP 请求和 numpy。4.3 完整代码极简 RAG首先定义两个核心函数一个用于调用 Embedding 接口把文本转成向量一个用于计算余弦相似度。# 文件路径llm-practice/rag_utils.py import requests import numpy as np import config def get_embedding(texts): 调用 OpenAI 兼容的 embeddings 接口 url f{config.BASE_URL}/embeddings headers { Authorization: fBearer {config.API_KEY}, Content-Type: application/json, } payload { model: text-embedding-3-small, input: texts if isinstance(texts, list) else [texts], } resp requests.post(url, headersheaders, jsonpayload, timeout60) resp.raise_for_status() data resp.json() return [item[embedding] for item in data[data]] def cosine_similarity(query_vec, doc_vec): 计算两个向量的余弦相似度 q np.array(query_vec) d np.array(doc_vec) return float(np.dot(q, d) / (np.linalg.norm(q) * np.linalg.norm(d) 1e-9))接下来准备一批模拟文档。实际项目中文档会从数据库或文件系统中读取这里直接放几个片段# 文件路径llm-practice/build_index.py import numpy as np from rag_utils import get_embedding documents [ RAG 是检索增强生成适合处理私有知识问答场景。, Agent 是能够自主调用工具、拆解任务的智能体。, MCP 是模型上下文协议用来标准化模型与外部工具之间的交互。, FP16 可以减少显存占用但部分场景下精度不如 FP32。, BF16 在保持较大动态范围的同时比 FP32 节省显存。, ] # 对文档做切块 chunks [] for doc in documents: # 简单切块每 20 个字符切一块实际项目中可以按段落或句号切 for i in range(0, len(doc), 20): chunk doc[i:i 20] chunks.append(chunk) # 批量向量化 chunk_vectors get_embedding(chunks) # 保存到字典 index { chunks: chunks, vectors: chunk_vectors, }检索函数也比较直接用 numpy 计算问题向量和所有 chunk 向量的相似度取 Top K# 文件路径llm-practice/retrieve.py import numpy as np from rag_utils import get_embedding, cosine_similarity def retrieve(query, index, top_k2): query_vec get_embedding(query)[0] scores [] for chunk, vec in zip(index[chunks], index[vectors]): scores.append((cosine_similarity(query_vec, vec), chunk)) scores.sort(keylambda x: x[0], reverseTrue) return [chunk for score, chunk in scores[:top_k]]最后把检索结果拼进系统提示词再调用对话模型# 文件路径llm-practice/rag_chat.py from chat import chat from build_index import index from retrieve import retrieve def rag_chat(question): related_chunks retrieve(question, index, top_k2) context \n.join([f- {c} for c in related_chunks]) prompt f请基于下面的资料回答用户问题。如果资料中没有相关内容就明确说不知道。 资料 {context} 用户问题{question} messages [ {role: system, content: 你是一个知识库问答助手只能根据资料回答。}, {role: user, content: prompt}, ] return chat(messages) if __name__ __main__: print(rag_chat(FP16 和 BF16 有什么区别))4.4 结果说明运行rag_chat.py时这个极简 RAG 会先把问题向量化与文档 chunks 做相似度检索再把最相关的两段资料拼进 Prompt。模型看到资料后会回答 “BF16 在保持较大动态范围的同时比 FP32 节省显存” 这类信息。这个示例很粗糙但已经能帮助你理解 RAG 的核心思想。实际工程中你需要考虑文档切块的粒度太短语义不完整太长上下文浪费向量数据库选型Chroma、Milvus、pgvector 等检索策略关键词检索、向量检索、混合检索重排模型对召回结果进一步排序。5. 进阶实战Agent 与工具调用5.1 让模型学会“查资料”LLM 本身只是文本生成模型它不具备实时查询天气、查询数据库、调用业务 API 的能力。Agent 的核心思想是给模型提供一组“工具描述”模型在回答过程中自主决定是否调用某个工具并根据工具返回结果继续生成回答。比如用户问“北京今天适合出门吗”模型可以根据工具描述调用get_weather拿到天气信息后再生成回复。这个过程中模型像是一个“调度器”工具则是它的“手脚”。5.2 Function Calling 示例OpenAI 兼容协议中工具调用通常通过tools参数声明。下面用 requests 实现一个简化版请求模型判断是否调用get_weather工具然后解析返回结果。# 文件路径llm-practice/tool_call.py import requests import json import config tools [ { type: function, function: { name: get_weather, description: 获取指定城市的天气情况, parameters: { type: object, properties: { city: {type: string, description: 城市名} }, required: [city] } } } ] def get_weather(city): # 示例函数实际项目中应调用真实天气服务 return f{city} 今天晴气温 24 摄氏度适合出门。 def call_llm_with_tools(messages): url f{config.BASE_URL}/chat/completions headers { Authorization: fBearer {config.API_KEY}, Content-Type: application/json, } payload { model: config.MODEL_NAME, messages: messages, tools: tools, tool_choice: auto, } resp requests.post(url, headersheaders, jsonpayload, timeout120) resp.raise_for_status() return resp.json() def run_agent(user_query): messages [{role: user, content: user_query}] response call_llm_with_tools(messages) message response[choices][0][message] # 判断模型是否请求调用工具 if message.get(tool_calls): for tool_call in message[tool_calls]: func_name tool_call[function][name] args json.loads(tool_call[function][arguments]) if func_name get_weather: observation get_weather(args[city]) messages.append({ role: tool, tool_call_id: tool_call[id], content: observation, }) # 将模型第一次回复的消息追加到上下文再让模型基于工具结果生成最终答案 messages.append(message) final_response call_llm_with_tools(messages) return final_response[choices][0][message][content] else: return message[content] if __name__ __main__: print(run_agent(北京今天适合出门吗))这个流程最关键的地方在于消息序列模型先返回一个tool_calls应用层执行对应工具后把工具结果以role: tool的消息追加到对话中然后再次调用模型。模型看到工具返回结果后才会生成面向用户的最终回答。5.3 MCP标准化工具协议上面这个 Function Calling 示例还有一个小问题如果模型要调用的是 10 个不同团队提供的服务每个服务的协议都不一样Agent 的代码会变得非常臃肿。MCPModel Context Protocol模型上下文协议就是为了解决这个问题而出现的。MCP 可以理解为模型与外部工具之间的“USB 接口”。它定义了客户端、服务器和工具之间的标准通信方式。一个 MCP Server 可以把数据库、文件系统、Web 服务统一暴露给 MCP ClientLLM 应用只需要连接 Client就能按标准格式发现并调用这些工具。MCP 目前还在快速演进中不同语言生态的 SDK 也不断变化所以本文不贴具体代码。你只需要先理解Agent是一种应用架构思想而MCP是这种架构中的标准化通信协议。5.4 编排框架的选择有人会问既然我可以用 requests 直接调用接口为什么还需要 LangChain、LlamaIndex 这类编排框架答案可以从两个角度看。如果你的业务逻辑非常简单比如只做一次 RAG直接用 HTTP 请求反而更容易控制和排错。但如果你的业务涉及多步推理、多工具编排、长期记忆、状态管理框架能帮你省去很多重复代码。框架提供的抽象能力包括Chain 流程编排、Memory 记忆管理、Retriever 统一接口、Callback 回调机制等。不过框架的缺点也很明显抽象层级越多越不容易理解底层发生了什么。建议初学者先用原生代码实现最小案例再用框架重写一遍这样你会知道框架帮你做了什么也更容易排查问题。6. 常见问题与排查思路LLM 应用开发过程中很多报错都是相似的。下面整理一份高频问题排查表问题现象常见原因解决思路401 UnauthorizedAPI Key 无效或未配置检查环境变量LLM_API_KEY确认密钥没有过期404 Not Found接口路径或模型名不存在确认BASE_URL是否包含/v1确认模型名与部署一致embedding 维度不一致文档向量和查询向量使用了不同模型统一使用同一个 Embedding 模型重新生成向量索引请求超时模型推理时间过长或网络原因调大timeout改用流式接口检查网络上下文长度超限输入文本超过模型上下文窗口压缩历史消息或使用摘要记忆减小文档 chunk 长度输出内容不稳定Prompt 指令不够具体增加格式要求、few-shot 示例降低 temperature中文回答质量差模型对中文指令理解不足尝试换中文能力更强的模型并用更明确的中文示例工具没有被调用模型认为不需要工具或工具描述不清晰检查工具描述是否准确适当使用强制调用参数tool_choice文本向量 API 未配置使用了 RAG 功能但没有配置LLM_EMBEDDING_API_KEY在配置文件中增加 Embedding 单独配置项并检查模型名排查问题时建议遵循“先看请求再看响应最后看日志”的顺序。第一次运行时可以打印请求 payload 和响应 status code确认接口地址、模型名、参数格式都没问题。多轮对话和工具调用中的问题还要额外检查 messages 列表是否合法比如tool_call_id是否对应正确。7. 最佳实践与工程建议7.1 精度选择FP16、FP32、BF16如果你要在本地部署模型精度选择直接影响显存占用和推理效果。FP3232 位浮点数精度最高但显存占用大适合小模型或调试场景。FP1616 位浮点数显存占用减半速度更快但数值范围较小训练或推理时可能出现精度溢出。BF16BFloat16用更多的指数位保留动态范围更适合大模型训练和推理在实践中比 FP16 更稳定。选择精度时可以先在 FP16/BF16 下跑一批测试用例和 FP32 结果对比。如果业务对输出质量要求很高不要为了省显存盲目降低精度。还可以利用量化方法比如 8-bit 和 4-bit但也要评估对效果的损失。7.2 Prompt 管理与版本化Prompt 不是随便写的自然语言它是应用的一部分。比较好的做法是统一模板管理Prompt 模板放在配置中心或版本库中不要散落在业务代码里版本化每次改动记录版本方便回滚和 A/B 测试包含输出格式明确要求模型输出 JSON、Markdown 或纯文本便于后续程序解析设置兜底在 Prompt 中要求模型“如果不知道就回答不知道”降低幻觉风险。7.3 成本与延迟控制LLM 应用上生产环境最大的两个隐患往往是成本和延迟。成本方面可以加入缓存层把相同问题的高频回复缓存起来也可以根据问题复杂度选择不同规格的模型简单问题用便宜模型复杂问题用强模型。合理设置max_tokens避免输出过长浪费 Token。延迟方面流式输出能显著改善用户体验把文档切块后的向量索引提前构建避免在请求链路上做重计算对 RAG 场景可以考虑使用更小的重排模型来提升响应速度。7.4 安全与合规涉及 LLM 应用时安全和合规问题不能忽略。权限最小化给服务配置的 API Key 应只具备所需权限不要使用管理员级密钥数据出网评估敏感业务数据如果要调用外部模型接口必须确认是否允许输入过滤对用户输入做长度限制和内容安全过滤避免恶意 Prompt 注入输出校验模型返回内容如果是结构化数据要做字段校验和异常兜底操作类工具必须授权比如数据库写入、系统操作必须在工具层做权限校验不能只看模型是否决定调用。7.5 测试与可观测性LLM 应用测试和传统应用测试不太一样。传统测试关注“对错”LLM 测试需要关注“质量和稳定性”。可以建立一套评测集把常见问题、边界问题和敏感问题都固定下来每次模型或 Prompt 变更后回归测试。同时要记录关键链路日志包括请求消息、工具调用、返回结果、Token 消耗、耗时等。没有日志你很难定位是 Prompt 问题、模型问题还是业务代码问题。8. 回到广告牌工程思维比热闹更重要旧金山那块 “human-powered LLM” 广告牌放在今天的语境下既是一种幽默也是一种提醒。它提醒我们用户看到的自然语言界面只是一个外壳真正决定产品价值的是外壳背后的工程链路是否可靠。你可能不会真的用一群人去模拟 LLM但如果不理解 RAG 的检索质量不关注模型精度和成本不建立 Prompt 管理和可观测机制你的程序也可能在用户面前 “假装智能”因为它的每一次回复都过于依赖运气。这篇文章从最小对话程序开始逐步实现了一个极简 RAG又通过 Function Calling 介绍了 Agent 的玩法。你可以把这些代码运行起来替换成自己的文档和工具然后尝试用 LangChain 或 LlamaIndex 重新实现一遍。你会发现当底层原理已经清楚时框架只是你工具箱里的螺丝刀而已。如果这篇文章对你有帮助记得收藏备用。接下来可以继续关注 MCP 的演进也可以深入了解本地推理引擎的优化方法。实践出真知动手跑一跑你会比只看广告牌的人懂得更多。
返回列表