
当你发现拼接 Prompt 已经无法让模型输出稳定格式,当 RAG 检索出的文档大半是噪声,当单轮问答无法完成“订机票 + 查天气 + 写邮件”的复合任务——恭喜你,你的大模型应用进入了“深水区”。 本文不教你怎么调用 API,而是从确定性输出、RAG 精准召回、Agent 规划与记忆、成本与延迟优化四个硬核维度,系统梳理一条从“Demo 玩具”到“生产级 Agent”的演进路径。所有方案均来自真实业务场景的反复试错。
大模型的“随机性”是应用开发的第一道坎。业务方要求返回 {"name": "张三", "age": 25},模型却给你 “好的,以下是 JSON 格式...” 加上 Markdown 代码块。
“你必须只返回一个合法的 JSON 对象,不要包含任何解释、前后缀或 Markdown 标记。”效果:约 70% 的请求能直接命中,但复杂 Schema 下失败率飙升。
使用 JSON Schema 强制生成,在采样阶段屏蔽不符合语法规则的 Token。推荐 outlines 库(比 jsonformer 更稳定):
import outlines
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0, "maximum": 120},
"hobbies": {"type": "array", "items": {"type": "string"}}
},
"required": ["name", "age"]
}
model = outlines.models.transformers("Qwen/Qwen2-7B-Instruct")
generator = outlines.generate.json(model, schema)
result = generator("提取以下文本中的姓名、年龄和爱好:张三,25岁,喜欢编程和游泳。")
print(result) # {'name': '张三', 'age': 25, 'hobbies': ['编程', '游泳']}原理:通过动态修改 logits,只允许生成符合 Schema 的 Token,从根源上杜绝非法 JSON。
即使使用约束解码,也可能因为模型未输出完整内容而解析失败。我们封装了一个“解析 + 修复”函数:
import json
import re
def safe_json_parse(text: str) -> dict:
# 1. 尝试直接解析
try:
return json.loads(text)
except:
pass
# 2. 移除 Markdown 代码块
cleaned = re.sub(r'```json\s*|\s*```', '', text)
# 3. 使用正则提取第一个完整 JSON 对象
match = re.search(r'\{.*\}', cleaned, re.DOTALL)
if match:
try:
return json.loads(match.group())
except:
pass
# 4. 利用 json-repair 修复常见错误(如末尾逗号)
from json_repair import repair_json
return json.loads(repair_json(cleaned))大多数 RAG 教程只教你把文档切块、Embedding、存向量库。但在生产环境中,召回精度和上下文相关度才是决定生成质量的命门。
单一的向量检索对罕见词(如特定产品型号)极不友好,而 BM25 擅长精确匹配。我们采用 倒排索引(Elasticsearch) + 向量索引(FAISS) 并行召回,然后融合排序。
from elasticsearch import Elasticsearch
import faiss
import numpy as np
def multi_recall(query: str, top_k: int = 10):
# 1. BM25 召回(ES)
es_body = {
"query": {
"multi_match": {"query": query, "fields": ["title^3", "content"], "operator": "or"}
},
"size": top_k * 2
}
es_hits = es.search(index="docs", body=es_body)["hits"]["hits"]
# 2. 向量召回(FAISS)
query_vec = embed_model.encode(query).reshape(1, -1)
distances, indices = faiss_index.search(query_vec, top_k * 2)
vec_docs = [all_docs[i] for i in indices[0]]
# 3. 合并去重 + RRF(Reciprocal Rank Fusion)重排
scores = {}
for rank, hit in enumerate(es_hits):
doc_id = hit["_id"]
scores[doc_id] = scores.get(doc_id, 0) + 1 / (60 + rank)
for rank, doc in enumerate(vec_docs):
doc_id = doc["id"]
scores[doc_id] = scores.get(doc_id, 0) + 1 / (60 + rank)
sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
return [doc_map[d] for d, _ in sorted_docs]向量召回往往包含大量“语义相近但实则无关”的文档。我们引入一个轻量级的交叉编码器(Cross-Encoder),如 BAAI/bge-reranker-v2-m3,对前 N 个候选进行精排。虽然它比向量模型慢,但只在 top 20 上运行,总体延迟可控。
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('BAAI/bge-reranker-v2-m3', max_length=512)
def rerank_docs(query: str, docs: list, top_k: int = 5):
pairs = [[query, d["content"]] for d in docs]
scores = reranker.predict(pairs) # 返回相关度分数
sorted_idx = np.argsort(scores)[::-1]
return [docs[i] for i in sorted_idx[:top_k]]效果:在客服场景中,Top-5 准确率从 65% 提升至 91%,显著减少幻觉。
即使使用了重排序,把 10 个冗长文档全部塞进上下文,不仅浪费 Token,还会因为Lost-in-the-Middle问题导致模型忽略中间内容。我们采用摘要压缩 + 位置加权策略:
def compress_docs(docs, max_tokens=2000):
# 将每个文档用 LLM 生成一句关键句(利用较小的模型,如 GPT-3.5-turbo)
summaries = []
for d in docs:
prompt = f"用一句话概括以下内容:{d['content'][:300]}"
summary = small_llm.generate(prompt)
summaries.append(summary)
# 将摘要拼接,并保证总长度不超过 max_tokens
return " ".join(summaries)[:max_tokens]Agent 是大模型应用的高级形态,需要规划(Planning) + 工具调用(Tool Use) + 记忆(Memory)。我们开发了一个用于“旅行规划”的多 Agent 系统。
使用 OpenAI Function Calling 格式,兼容大多数开源模型(如 Qwen、GLM)。
tools = [
{
"type": "function",
"function": {
"name": "search_flights",
"description": "查询航班信息",
"parameters": {
"type": "object",
"properties": {
"origin": {"type": "string"},
"destination": {"type": "string"},
"date": {"type": "string", "format": "date"}
},
"required": ["origin", "destination"]
}
}
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询目的地天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}
]我们实现一个轻量级 ReAct 引擎,支持多轮工具调用和结果聚合。
class SimpleAgent:
def __init__(self, model, tools, max_iter=5):
self.model = model
self.tools = tools
self.max_iter = max_iter
self.memory = [] # 对话历史
def run(self, user_query):
self.memory.append({"role": "user", "content": user_query})
for _ in range(self.max_iter):
response = self.model.chat(self.memory, tools=self.tools)
# 检查是否要求调用工具
if response.get("tool_calls"):
for call in response["tool_calls"]:
tool_name = call["function"]["name"]
args = json.loads(call["function"]["arguments"])
result = self.execute_tool(tool_name, args)
self.memory.append({
"role": "tool",
"tool_call_id": call["id"],
"content": json.dumps(result)
})
else:
# 没有工具调用,直接返回最终答案
self.memory.append({"role": "assistant", "content": response["content"]})
return response["content"]
return "任务执行超时,请简化需求。"关键优化:
对于个人助手类 Agent,我们需要记住用户的历史偏好(如“喜欢靠窗座位”、“不吃海鲜”)。每次调用 Agent 时,先从向量库检索该用户的近期记忆,注入 system prompt。
user_memories = vector_db.search(user_id, top_k=5) # 返回偏好条目
memory_prompt = "该用户的历史偏好:" + ";".join([m["content"] for m in user_memories])
system_prompt = f"你是一个智能助手。{memory_prompt}。请根据用户当前需求提供个性化服务。"LLM 推理的费用和延迟是商业落地的最大障碍。我们通过以下组合拳实现降本增效:
对于固定的系统指令和少量固定文档,我们利用模型的 KV Cache 复用(如 OpenAI 的 Prompt Caching 或开源的 cache 机制)。若前缀完全相同,则不需重算 Attention 的 Key/Value。
并非所有请求都需要“满血版”模型。我们在网关层增加一个意图分类器(轻量级 BERT),将简单问答路由到 7B 模型(延迟 200ms,费用低 10 倍),复杂推理路由到 70B 模型。
class Router:
def __init__(self):
self.classifier = pipeline("text-classification", model="small-intent-model")
def route(self, query):
label = self.classifier(query)[0]["label"]
if label in ["问候", "常识", "翻译"]:
return "qwen2-7b"
else:
return "qwen2-72b"对于交互式应用(如聊天),TTFT 至关重要。我们强制使用流式输出(SSE),且开启 Speculative Decoding(推测解码):用小模型快速生成候选 Token,大模型只做验证,可将生成速度提升 2~3 倍。
# vLLM 支持推测解码
llm = LLM(model="meta-llama/Llama-2-70b-chat",
speculative_model="meta-llama/Llama-2-7b-chat",
num_speculative_tokens=5) # 每次预测 5 个候选大模型应用的错误难以复现,必须建立完善的可观测体系。
每次请求记录:
prompt 完整内容(含 system 和 tools)response 或 tool_callstoken_usage(input / output / total)latency_breakdown(检索耗时、模型推理耗时、工具执行耗时)feedback(用户点赞/点踩,用于后续 RLHF)构建一个包含 200+ 典型问题的测试集,每次模型升级或 Prompt 修改后,自动运行评估,用更强的模型(如 GPT-4)对输出质量打分(准确性、相关性、格式合规),低于阈值则阻断上线。
def evaluate_answer(query, answer, reference=None):
judge_prompt = f"""你是一个严格的评审专家。请对以下回答进行评分(1-10分):
问题:{query}
回答:{answer}
参考:{reference or "无"}
请返回 JSON:{{"score": 8, "reason": "..."}}"""
return llm_generate(judge_prompt, model="gpt-4")背景:某电商平台需要将人工客服的 40% 问答自动化。
V1(原始方案):直接 Prompt + 向量检索(Q&A 对),准确率 62%,用户满意度 3.2/5。
V2(结构化输出 + 混合召回):强制 JSON 返回意图和实体,召回引入 BM25+向量,准确率 78%,满意度 3.8/5。
V3(Agent + 工具):允许 Agent 调用订单查询、退换货申请、物流追踪等 6 个 API,准确率 89%,满意度 4.2/5。但平均延迟从 1.2s 升至 3.5s。
V4(优化版):
大模型应用开发正在从“文本生成”走向“任务执行”。要构建真正可用的产品,我们必须:
未来的大模型应用将更加“无感知”——模型自主决策何时调用工具、如何规划步骤,甚至能够自我反思纠错。作为开发者,我们既要拥抱模型的进化,也要在工程层面扎好篱笆,确保每一次生成都可控、可追溯、可优化。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。