ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Python开发者LLM应用开发实战:从API调用到文档问答系统

Python开发者LLM应用开发实战:从API调用到文档问答系统 1. 为什么现在入局 LLM 应用开发正当时如果你是一个 Python 开发者最近半年大概率会有一种感觉身边所有人都在聊大模型但真正把 LLM 跑进自己项目里的人并不多。大多数人停留在“用网页版问几个问题”的阶段少数人试过调 API但很快就卡在“然后呢”这个问题上——调用一次接口返回一段文本这玩意儿到底怎么变成产品我自己的经历比较典型。去年年底开始我把手上几个 Python 项目陆续接入了 LLM 能力从最简单的文本摘要到后来的多轮对话系统踩了不少坑也总结了一些真正能落地的经验。这篇文章就是把这些经验整理出来给那些已经会写 Python、但对 LLM 应用开发还没有系统认知的朋友一条清晰的路径。先说清楚这篇文章的定位它不是教你训练大模型也不是讲 Transformer 的数学原理。它解决的是一个非常具体的问题——你已经有 Python 基础现在想把 LLM 的能力嵌入到自己的开发工作流和实际应用里应该从哪里开始、用什么工具、避开哪些坑。适合的读者是写过 Python 脚本、了解基本的 HTTP 请求和 JSON 处理、但对 LLM 生态还比较陌生的开发者。如果你已经能熟练使用 LangChain 搭建复杂 Agent这篇文章可能对你偏基础但里面的实操细节和避坑经验仍然值得扫一眼。核心关键词就四个Python、LLM、开发工作流、应用。我会围绕这四个词展开把从环境搭建到第一个可用应用的完整链路讲透。2. 开发环境搭建与工具链选型2.1 Python 版本选择与虚拟环境管理LLM 相关的 Python 库对版本有一定要求。我实测下来Python 3.10 或 3.11 是目前最稳妥的选择。3.9 虽然也能跑大部分库但一些新出的框架已经开始放弃对 3.9 的支持3.12 则因为部分底层依赖比如某些 tokenizer 的 C 扩展还没完全适配偶尔会遇到安装失败的问题。虚拟环境这块如果你还在用全局 Python 装包强烈建议改掉这个习惯。LLM 生态的依赖冲突非常频繁——比如transformers和vllm对pydantic的版本要求经常打架。我习惯用venv加pip的组合简单直接python -m venv llm-env source llm-env/bin/activate # Linux/Mac # llm-env\Scripts\activate # Windows如果你需要管理多个 LLM 项目conda或者poetry会更方便。poetry的优势在于它的锁文件机制能确保依赖版本可复现这在 LLM 项目里特别重要——同一个模型在不同版本的transformers下输出可能不一样。注意不要在生产环境直接用pip install装最新版。LLM 库的 breaking change 非常频繁建议在requirements.txt里锁定具体版本号比如openai1.30.0而不是openai。2.2 LLM 接入方式的选择API 还是本地部署这是新手面临的第一个关键决策。两条路各有适用场景我整理了一个对比表维度API 调用本地部署硬件要求几乎为零需要 GPU至少 8GB 显存成本模式按 token 付费一次性硬件投入延迟取决于网络通常 1-3 秒取决于硬件可优化到毫秒级数据隐私数据出本地完全本地模型选择受限于服务商任意开源模型维护成本低高需要处理显存、并发等问题我的建议是先用 API 跑通流程再根据实际需求决定是否转本地。大部分应用场景下API 的成本和延迟都是可接受的。只有当你需要处理敏感数据、或者调用量极大导致 API 成本超过硬件成本时才值得考虑本地部署。API 接入方面OpenAI 的接口是目前事实上的标准很多国产模型也兼容这套接口格式。安装官方 SDKpip install openai一个最小的调用示例from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttps://api.openai.com/v1 # 如果使用兼容接口改这里 ) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个 Python 编程助手。}, {role: user, content: 解释一下 Python 的装饰器} ], temperature0.7 ) print(response.choices[0].message.content)这段代码里有几个关键参数需要理解。model决定了你用哪个模型不同模型的能力和价格差异很大。messages是一个列表包含了对话历史这是 LLM 对话的核心机制——模型本身不记忆每次调用都需要把历史传进去。temperature控制输出的随机性0 表示最确定性的输出1 以上会变得很有创意但也容易跑偏。写代码相关的任务建议用 0.2-0.5创意写作可以用 0.8-1.0。2.3 本地部署的入门方案如果你决定走本地部署路线最省心的入门工具是Ollama。它把模型下载、量化、推理服务都封装好了一条命令就能跑起来# 安装后拉取模型 ollama pull llama3.1:8b # 启动服务 ollama serve然后 Python 这边通过 HTTP 请求调用import requests response requests.post( http://localhost:11434/api/generate, json{ model: llama3.1:8b, prompt: 用 Python 写一个快速排序, stream: False } ) print(response.json()[response])Ollama 的优点是上手极快缺点是并发能力弱不适合生产环境。如果要上生产需要考虑vllm或TGI这类推理框架但那是另一个话题了。实操心得本地部署时模型量化版本如 Q4_K_M和原始版本的输出质量差距比想象中小但显存占用能减少一半以上。8GB 显存的卡跑 7B 模型的 Q4 量化版完全够用。3. 把 LLM 嵌入开发工作流的核心模式3.1 从“调用一次”到“工作流”的思维转变很多人第一次调通 API 之后下一步就不知道干什么了。原因在于他们把 LLM 当成一个“问答机器”而不是一个“工作流组件”。这两者的区别很大问答机器是你问一句它答一句工作流组件是它在你已有的流程中承担某个环节的智能处理。举个具体例子。假设你有一个 Python 脚本每天从某个数据源拉取一批用户反馈存到数据库里。传统做法是你写规则来分类这些反馈。接入 LLM 之后你可以让模型来分类但关键不在于“调用模型分类”而在于怎么把模型调用嵌入到已有的数据处理管道里import json from openai import OpenAI client OpenAI() def classify_feedback(feedback_text): 将用户反馈分类到预定义的类别中 response client.chat.completions.create( modelgpt-4o-mini, messages[ { role: system, content: 你是一个反馈分类助手。将用户反馈分类到以下类别之一功能请求、Bug报告、使用咨询、投诉、其他。只返回类别名称不要解释。 }, {role: user, content: feedback_text} ], temperature0 ) return response.choices[0].message.content.strip() def process_feedback_batch(feedback_list): 批量处理反馈 results [] for fb in feedback_list: category classify_feedback(fb[text]) results.append({ id: fb[id], text: fb[text], category: category }) return results这个模式的核心是LLM 负责它擅长的模糊判断Python 负责它擅长的流程控制。分类结果可能不完美但你可以加一层校验逻辑比如检查返回的类别是否在预定义列表中不在就重试或标记为待人工处理。3.2 结构化输出让 LLM 返回可编程的数据LLM 默认返回的是自然语言文本但你的程序需要的是结构化数据。这是新手最容易卡住的地方。解决方案有三种我按推荐程度排序第一种JSON 模式。OpenAI 的 API 支持response_format{type: json_object}强制模型返回合法 JSONresponse client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 提取用户信息以 JSON 格式返回包含 name、age、city 三个字段。}, {role: user, content: 我叫张三今年 28 岁住在杭州。} ], response_format{type: json_object}, temperature0 ) data json.loads(response.choices[0].message.content) print(data) # {name: 张三, age: 28, city: 杭州}第二种函数调用Function Calling。这是更强大的方式你定义好函数的参数 schema模型会自动判断是否需要调用以及传什么参数。适合需要模型触发具体操作的场景。第三种提示词约束加后处理。在不支持 JSON 模式的模型上你可以在提示词里明确要求返回 JSON然后用正则表达式提取。这种方式最不稳定但兼容性最好。注意即使开了 JSON 模式模型偶尔也会返回不符合你预期 schema 的 JSON。生产环境一定要加 try-except 和字段校验不要假设模型每次都返回完美结果。3.3 流式输出提升用户体验的关键如果你的应用有前端界面流式输出几乎是必须的。用户等 5 秒看到完整回复和逐字看到回复体验差距巨大。Python 这边处理流式输出stream client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 写一首关于编程的诗}], streamTrue ) for chunk in stream: if chunk.choices[0].delta.content is not None: print(chunk.choices[0].delta.content, end, flushTrue)流式输出的原理是服务端每生成一个 token 就推送给客户端而不是等全部生成完再返回。这对长文本场景特别重要——生成 1000 字的回复可能需要 10 秒流式输出能让用户在第 1 秒就开始看到内容。4. 构建第一个可用的 LLM 应用4.1 应用场景选择从“文档问答”入手如果你不知道第一个应用做什么我推荐从文档问答开始。原因有三需求明确、技术链路完整、容易验证效果。具体来说就是让用户上传一个文档然后针对文档内容提问LLM 基于文档内容回答。这个场景涉及了 LLM 应用开发的核心技术点文本切分、向量化、相似度检索、上下文拼接、生成回答。走通一遍你对整个链路就有感觉了。4.2 核心实现不依赖框架的纯 Python 版本市面上有很多 RAG 框架LangChain、LlamaIndex 等但我建议第一遍用纯 Python 实现这样你能真正理解每一步在做什么。下面是一个最小可运行版本import numpy as np from openai import OpenAI client OpenAI() def get_embedding(text): 获取文本的向量表示 response client.embeddings.create( modeltext-embedding-3-small, inputtext ) return response.data[0].embedding def split_text(text, chunk_size500, overlap50): 将长文本切分为重叠的块 chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) start end - overlap return chunks def cosine_similarity(a, b): 计算余弦相似度 a, b np.array(a), np.array(b) return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) class SimpleDocQA: def __init__(self, document): self.chunks split_text(document) self.embeddings [get_embedding(chunk) for chunk in self.chunks] def query(self, question, top_k3): # 1. 将问题向量化 q_embedding get_embedding(question) # 2. 计算与每个块的相似度 similarities [ cosine_similarity(q_embedding, emb) for emb in self.embeddings ] # 3. 取最相关的 top_k 个块 top_indices np.argsort(similarities)[-top_k:][::-1] context \n\n.join([self.chunks[i] for i in top_indices]) # 4. 拼接上下文和问题调用 LLM response client.chat.completions.create( modelgpt-4o-mini, messages[ { role: system, content: f基于以下文档内容回答问题。如果文档中没有相关信息直接说不知道。\n\n文档内容\n{context} }, {role: user, content: question} ], temperature0.3 ) return response.choices[0].message.content这段代码虽然简单但包含了 RAG 的完整链路。chunk_size和overlap的选择有讲究块太小会丢失上下文块太大会引入无关信息。500 字加 50 字重叠是我实测下来比较通用的配置但具体要根据文档类型调整——技术文档可以小一些叙事性文档需要大一些。4.3 参数计算与选择依据上面代码里几个关键参数的选择逻辑值得展开说chunk_size 的确定。这个值取决于你的嵌入模型的最大输入长度和 LLM 的上下文窗口。text-embedding-3-small支持 8191 个 token但实际使用中不需要用满。500 个字符大约对应 200-300 个 token这个粒度既能保留足够的语义信息又不会让检索结果过于笼统。top_k 的选择。取 3 个块是经验值。取太少可能漏掉关键信息取太多会引入噪声并且增加 token 消耗。你可以做一个简单的实验准备 20 个问题分别用 top_k1、3、5 跑一遍看回答准确率的变化。我自己的测试中top_k 从 1 增加到 3 提升明显从 3 增加到 5 提升有限但成本增加 60% 以上。temperature 的设置。文档问答场景建议用 0.1-0.3。这个场景需要的是忠实于文档内容而不是创意发挥。温度太高会导致模型“脑补”文档里没有的内容。4.4 从脚本到应用加上 Web 界面纯脚本只能自己用要变成别人也能用的应用需要加一层 Web 界面。最轻量的方案是Streamlitpip install streamlitimport streamlit as st st.title(文档问答助手) uploaded_file st.file_uploader(上传文档, type[txt, md]) if uploaded_file: document uploaded_file.read().decode(utf-8) if qa not in st.session_state: with st.spinner(正在处理文档...): st.session_state.qa SimpleDocQA(document) question st.text_input(输入你的问题) if question: with st.spinner(思考中...): answer st.session_state.qa.query(question) st.write(answer)Streamlit 的好处是你不需要写 HTML/CSS/JavaScript纯 Python 就能出一个可交互的界面。对于内部工具和原型验证来说效率极高。实操心得st.session_state是 Streamlit 应用的关键。因为 Streamlit 每次交互都会重新运行整个脚本如果不把处理好的文档存到 session_state 里每次提问都会重新处理一遍文档既慢又费钱。5. 常见问题与排查技巧实录5.1 API 调用类问题问题一请求超时。LLM API 的响应时间波动很大尤其是长文本生成。默认超时时间往往不够。解决方案是显式设置超时并加重试from openai import OpenAI import httpx client OpenAI( timeouthttpx.Timeout(60.0, connect10.0), max_retries3 )问题二速率限制Rate Limit。免费额度或低档付费方案通常有 RPM每分钟请求数和 TPM每分钟 token 数限制。批量处理时很容易触发。我的做法是加一个简单的令牌桶限流器或者直接用tenacity库做指数退避重试from tenacity import retry, wait_exponential, stop_after_attempt retry(waitwait_exponential(multiplier1, min4, max60), stopstop_after_attempt(5)) def safe_llm_call(messages): return client.chat.completions.create( modelgpt-4o-mini, messagesmessages )问题三返回内容被截断。默认的max_tokens可能不够。注意max_tokens限制的是输出长度不是输入长度。如果你需要长输出显式设置这个参数但要注意不同模型的上限不同。5.2 输出质量类问题问题四模型不按格式返回。你要求返回 JSON它给你返回一段解释加 JSON。解决办法是在 system prompt 里用更强的约束语言比如“只返回 JSON不要有任何其他文字”同时开启 JSON 模式。如果还是不行加一个后处理步骤用正则提取 JSON 部分。问题五模型“幻觉”。在文档问答场景中模型可能会编造文档里没有的内容。缓解方法有三层第一在 prompt 里明确说“如果文档中没有相关信息直接说不知道”第二降低 temperature第三在回答中要求模型引用原文片段方便你验证。问题六中文输出夹杂英文。这在用英文 prompt 时很常见。解决办法是 system prompt 用中文写并明确要求“用中文回答”。5.3 成本控制类问题问题七token 消耗过快。几个容易忽略的消耗点对话历史越来越长、system prompt 太长、检索返回的上下文太多。优化手段包括定期截断对话历史只保留最近 N 轮、精简 system prompt、调整 top_k 和 chunk_size。问题八嵌入计算的成本。如果你有大量文档需要处理嵌入计算的成本可能超过生成回答的成本。建议对文档做增量处理——只对新文档计算嵌入已有的缓存起来。可以用简单的文件哈希做去重。问题类型典型表现首选解决方案超时请求长时间无响应设置 timeout 参数加自动重试限流返回 429 错误指数退避重试加请求间隔截断输出不完整显式设置 max_tokens格式错误返回非 JSON开启 JSON 模式加后处理校验幻觉编造不存在的信息降低 temperature 加 prompt 约束成本高账单超预期缓存嵌入加精简上下文5.4 几个我踩过的坑第一个坑是在循环里反复创建 OpenAI client。这个 client 内部维护了连接池反复创建会导致连接无法复用性能下降明显。正确做法是在模块级别创建一个全局 client所有函数共用。第二个坑是忽略编码问题。处理中文文档时如果文件不是 UTF-8 编码读取会报错或乱码。建议统一用encodingutf-8读取遇到非 UTF-8 文件先转换。第三个坑是把 API key 硬编码在代码里。这个不用多说了用环境变量或者.env文件管理。.env文件记得加到.gitignore里。第四个坑是没有做输入长度检查。用户可能粘贴一篇几万字的文章直接传给 API 会报错。在调用前加一个长度检查超长的先做摘要或截断。6. 下一步可以往哪里走走通上面这条链路之后你已经有能力把 LLM 嵌入到实际的 Python 项目里了。接下来可以根据具体需求往几个方向深入需要处理更复杂的对话逻辑可以了解 LangChain 或 LlamaIndex 的链式调用需要让模型调用外部工具可以研究 Function Calling 和 Agent 模式需要处理大量文档可以看看向量数据库如 Chroma、Qdrant的用法需要本地部署可以深入 Ollama 或 vllm 的配置优化。我个人在实际操作中的体会是LLM 应用开发最难的部分不是调通 API而是设计好人和模型的分工边界。哪些判断交给模型哪些逻辑用代码写死这个边界划清楚了应用就稳了。划不清楚就会出现“模型偶尔抽风导致整个流程崩溃”的情况。一个实用的原则是模型负责生成候选结果代码负责校验和兜底。永远不要假设模型 100% 按你的预期输出。
RELATED READING

延伸阅读

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