Python LLM 应用开发已经不只是“把问题发给模型,再打印答案”。一个可用的系统通常横跨模型 API、提示词设计、RAG、智能体、工具调用与 MCP,还要处理超时、成本、安全和评测。真正值得检验的,不是记住多少术语,而是能否把这些组件组合成一个行为可预测、故障可定位的应用。
从一次 API 调用开始,但不要停在那里
最基础的能力,是稳定地调用模型 API。除了请求格式,还应考虑环境变量、超时、错误响应、模型配置和结构化输出。
下面是一个可直接改造的 Python 示例。它假设模型服务提供与 OpenAI Chat Completions 类似的 HTTP 接口;运行前需要按实际供应商修改 LLM_BASE_URL、LLM_MODEL 和密钥。
import json
import os
import sys
import urllib.error
import urllib.request
BASE_URL = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1")
API_KEY = os.environ["LLM_API_KEY"]
MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini")
def ask_model(question: str) -> str:
payload = {
"model": MODEL,
"temperature": 0,
"messages": [
{
"role": "system",
"content": (
"你是 Python 技术助手。回答必须简洁;"
"如果信息不足,明确说明缺少什么,不要编造。"
),
},
{
"role": "user",
"content": f"请回答下面的问题:\n<question>\n{question}\n</question>",
},
],
}
request = urllib.request.Request(
f"{BASE_URL.rstrip('/')}/chat/completions",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode("utf-8"))
return result["choices"][0]["message"]["content"]
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"模型 API 返回 {exc.code}: {detail}") from exc
if __name__ == "__main__":
question = " ".join(sys.argv[1:]) or "解释 Python 生成器适合解决什么问题"
print(ask_model(question))
运行方式:
export LLM_API_KEY="替换为你的密钥"
export LLM_BASE_URL="https://api.openai.com/v1"
export LLM_MODEL="gpt-4o-mini"
python app.py "RAG 与模型微调有什么区别?"
这段代码只是基线。进入生产环境后,还需要增加重试与指数退避、速率限制、请求追踪、令牌用量记录,以及对响应字段缺失或格式异常的处理。
提示词也不应被当成一段随意拼接的字符串。系统规则、用户输入、检索资料和工具返回值应有清晰边界。尤其不要把检索到的网页内容直接视为可信指令,因为其中可能包含提示词注入文本。
RAG 的难点在检索链路,而不只是向量数据库
RAG,也就是检索增强生成,解决的是“回答前先找到相关外部资料”的问题。一个完整链路通常可以拆成:
- 文档解析与清洗;
- 分块并保存来源、标题、时间等元数据;
- 为查询召回候选内容;
- 过滤或重排候选片段;
- 将证据交给模型生成答案;
- 返回引用,并评估答案是否受证据支持。
判断一个 RAG 系统时,应把“没找到资料”和“模型没有正确使用资料”区分开。前者属于检索问题,后者更接近生成或提示词问题。只检查最终答案,很难定位故障发生在哪一层。
可以用下面的测试集格式保存最小评测数据,并在更换分块策略、嵌入模型或重排器后重复执行:
{"question":"退款申请需要哪些材料?","expected_doc_ids":["policy-refund-2025"],"must_include":["订单号","付款凭证"]}
{"question":"企业账户如何修改管理员?","expected_doc_ids":["account-admin-guide"],"must_include":["身份验证"]}
至少分别记录两类指标:检索阶段是否命中预期文档,以及最终回答是否覆盖必要事实。这样可以避免用“答案看起来不错”代替可重复的评测。
智能体和 MCP:一个负责决策,一个负责连接
智能体并不等于一段更长的提示词。它通常包含循环:模型观察当前状态、选择动作、调用工具、读取结果,再决定继续还是结束。循环带来了能力,也带来了新的风险:重复调用、成本失控、越权操作,以及把恶意工具输出当成可信指令。
MCP 可以放在另一层理解:它为模型应用发现和使用外部上下文或工具提供标准化连接方式。MCP 本身不替应用决定业务权限,也不会自动解决工具是否安全的问题。即使工具通过统一协议接入,应用仍要执行身份认证、参数验证、审计和授权。
可以这样定义一份与具体框架无关的工具策略,并在智能体执行器中强制实施:
agent_policy:
max_steps: 6
timeout_seconds: 45
require_confirmation:
- delete_file
- send_email
- create_payment
allowed_tools:
- search_docs
- get_order_status
- draft_email
limits:
search_docs: 5
get_order_status: 3
redact_from_logs:
- api_key
- access_token
- customer_phone
关键点是把权限控制放在确定性的应用代码中,而不是仅靠提示词要求模型“谨慎操作”。只读工具可以默认开放;写操作、高成本操作和不可逆操作应增加人工确认或事务审批。
上线前,用这些问题做一次自测
可以用下面的清单检验自己是否真正掌握了 Python LLM 应用开发:
- 模型 API 超时或返回 429、500 时,程序会怎样处理?
- 提示词、模型版本和推理参数是否可以追踪?
- 输出是否需要 JSON Schema 或业务规则校验?
- RAG 失败时,能否判断是解析、分块、召回、重排还是生成出了问题?
- 检索结果是否携带来源,用户能否核对证据?
- 智能体是否有最大步数、超时、预算和工具白名单?
- 工具参数是否在服务端再次验证?
- MCP 服务不可用或返回恶意内容时,应用是否能安全降级?
- 日志中是否意外保存了密钥、个人信息或完整对话?
- 是否存在一组固定测试,用于比较提示词、模型和检索策略的变更?
成熟的 LLM 应用不是依赖一次“聪明回答”,而是依赖可观测、可评测、可限制的工程流程。建议从单次 API 调用开始,随后加入结构化输出和测试,再逐步引入 RAG、工具和智能体。每增加一层自主性,都应同步增加权限边界、失败处理和审计能力。