大模型快速入门,基顾入门编
总体路线图
一个能上线的大模型 Agent 应用,通常经历 7 个阶段:
模型选用
→ 模型下载与本地部署(或接入云端 API)
→ 工具编写:Function Calling / MCP 服务
→ 知识增强:RAG 检索
→ 单 Agent 封装(+ Skills 技能包)
→ 多 Agent 编排:Swarm / Supervisor / Graph API
→ 安全护栏:PII / 黑名单 / HITL / 输出审核
→ 评估、持久化、审计、灰度发布
五层心智模型(比原笔记的"一句话原则"更好记):
|
层 |
角色 |
类比 |
|---|---|---|
|
模型层 |
LLM 本体 |
大脑 |
|
接入层 |
OpenAI 兼容 API / vLLM 服务 |
神经接口 |
|
能力层 |
工具、MCP、Skills、RAG |
手和工具箱、岗位手册、资料室 |
|
编排层 |
Agent、Swarm、Supervisor、Graph |
岗位、班组、主管与流程图 |
|
治理层 |
Guardrails、评估、审计 |
安检、绩效考核与刹车 |
核心原则不变:工具是零件,Agent 是岗位,编排是班组与流程,Guardrails 是安检与刹车。
1. 模型选用:先按任务选模型,再按并发选框架
1.1 按任务类型选模型
|
模型类型 |
能力特点 |
代表模型(2026 年主流) |
适用场景 |
|---|---|---|---|
|
通用语言模型 |
推理、生成、工具调用 |
DeepSeek-V3.x / R1、Qwen3 系列、Kimi K2、GLM-4.x、GPT-5 系列、Claude 4.x |
问答、Agent、代码的主力模型 |
|
多模态模型 |
图/文/音/视频理解 |
Qwen-VL 系列、GPT-4o/5 系列、GLM-4V |
图文理解、语音交互 |
|
文本嵌入模型 |
文本转向量 |
BGE-M3、Qwen3-Embedding、text-embedding-v3 |
RAG 召回、语义搜索、去重 |
|
重排序模型 |
对召回结果精排 |
bge-reranker-v2-m3 等 |
RAG 二阶段排序,显著提升命中率 |
|
多模态嵌入模型 |
跨模态同一向量空间 |
Chinese-CLIP、GME 系列 |
文搜图、图搜图 |
|
文档解析模型 |
OCR、版面、表格抽取 |
Dots.OCR、MonkeyOCR、PaddleOCR-VL、MinerU |
PDF/扫描件/票据结构化 |
|
垂直领域模型 |
医疗、金融、安全等 |
AlphaFold、各行业大模型 |
强专业、强合规场景 |
经验:RAG 项目里,Embedding、文档解析和 Rerank 模型经常比生成模型更影响最终效果,不要只盯着"最大最新的生成模型"。另外,Agent 场景务必确认模型支持 Function Calling(工具调用),这是硬门槛。
1.2 闭源 API 还是开源自部署?
|
维度 |
闭源 API(DeepSeek/通义/Kimi/OpenAI…) |
开源自部署(Qwen、DeepSeek 开源版…) |
|---|---|---|
|
上手成本 |
注册即得 Key,零运维 |
需要 GPU、部署、运维 |
|
效果上限 |
旗舰模型效果最强 |
同尺寸下略逊,但迭代极快 |
|
数据合规 |
数据出域,需评估 |
数据不出内网,合规友好 |
|
成本结构 |
按 token 计费,低频便宜 |
固定显卡成本,高频摊薄更省 |
|
可定制性 |
只能调 prompt/参数 |
可微调、可量化、可改架构 |
建议:学习和原型阶段直接用闭源 API(DeepSeek 等价格极低);数据敏感或调用量大的生产场景再考虑自部署 Qwen/DeepSeek 开源权重。
1.3 本地推理框架对比
|
推理框架 |
出品方 |
特点 |
适合 |
|---|---|---|---|
|
vLLM |
UC Berkeley |
PagedAttention 高效管理 KV Cache,显存利用率高,支持张量/流水线并行,自带 OpenAI 兼容服务 |
高并发服务化部署首选 |
|
SGLang |
LMSYS 团队 |
Radix Tree 复用 KV 前缀,长多轮对话、结构化输出强 |
长上下文、多轮对话 |
|
TensorRT-LLM |
NVIDIA |
CUDA 内核级优化,FP8/INT4 量化 |
NVIDIA 平台极限性能 |
|
LMDeploy |
上海 AI Lab |
量化工具链完整,国产芯片/昇腾优化 |
国产化、私有化 |
|
Transformers |
Hugging Face |
加载/微调/推理全流程,生态最广 |
研究、开发底座,不适合高并发 |
|
Ollama / llama.cpp |
社区 |
一行命令跑模型,CPU/消费级显卡友好 |
个人学习、轻量试用 |
Ollama 适合个人学习;企业生产关注并发、吞吐、显存利用率、审计能力,常选 vLLM / SGLang / TensorRT-LLM / LMDeploy。
2. 模型下载与配置:从拉权重到跑起服务
2.1 下载渠道与命令
|
渠道 |
说明 |
|---|---|
|
ModelScope 魔搭 |
国内访问稳定,国产模型齐全, |
|
Hugging Face Hub |
全球最大开源模型社区,国内可用镜像 |
|
企业内部制品库 |
生产推荐:版本固化、灰度、审计、离线分发 |
# 方式一:ModelScope(国内推荐)
modelscope download --model Qwen/Qwen3-8B --local_dir ./models/Qwen3-8B
# 方式二:Hugging Face + 国内镜像
HF_ENDPOINT=https://hf-mirror.com hf download Qwen/Qwen3-8B --local-dir ./models/Qwen3-8B
2.2 下载前检查清单
config.json(模型结构)、tokenizer.json/tokenizer_config.json(分词器)*.safetensors权重文件(或 GGUF/AWQ 等量化权重)generation_config.json(默认生成参数)- LICENSE / 模型卡:是否允许商用、是否需署名、是否有行业/用途/分发限制
镜像下载务必校验文件大小与 SHA/etag;企业环境落内网制品库,避免上线时"HF 抖动"导致发布失败。
2.3 显存估算(决定你能不能跑)
推理显存 ≈ 参数量 × 每参数字节数 × 1.2(冗余) + KV Cache(随并发和上下文增长)
FP16/BF16:每参数 2 字节 → 8B 模型 ≈ 16GB + KV → 24GB 卡(如 4090)可跑
INT8 量化:每参数 1 字节 → 8B 模型 ≈ 8GB
INT4 量化:每参数 0.5 字节 → 8B 模型 ≈ 4~5GB,2060 也能跑
经验法则:显存不够就降量化精度,再不够就换小模型,别硬上。
2.4 方案 A:Ollama 三分钟跑起来(个人学习)
# 安装后一行命令
ollama run qwen3:8b
# 同时会在 11434 端口暴露 OpenAI 兼容接口: http://localhost:11434/v1
2.5 方案 B:vLLM 起生产级 OpenAI 兼容服务
pip install vllm
vllm serve ./models/Qwen3-8B \
--served-model-name Qwen3-8B \
--max-model-len 32768 \
--gpu-memory-utilization 0.9 \
--port 8000
验证服务:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "Qwen3-8B", "messages": [{"role": "user", "content": "你好"}]}'
2.6 统一接入层:一个文件打通云端与本地
这是原笔记最大的坑——deepseek_llm 从来没人定义。我们把它补全,并且设计成云端 API 和本地 vLLM 一键切换(因为两者都讲 OpenAI 兼容协议):
# init_llm.py —— 全文所有示例的统一入口
import os
from openai import OpenAI
from langchain_openai import ChatOpenAI
# 云端示例:DeepSeek 官方 API(也可换成通义百炼、Kimi 等任意 OpenAI 兼容服务)
API_KEY = os.getenv("LLM_API_KEY", "sk-换成你的key")
BASE_URL = os.getenv("LLM_BASE_URL", "https://api.deepseek.com")
MODEL = os.getenv("LLM_MODEL", "deepseek-chat")
# 本地 vLLM / Ollama 只需改两个环境变量,业务代码零改动:
# export LLM_BASE_URL=http://127.0.0.1:8000/v1
# export LLM_MODEL=Qwen3-8B
client = OpenAI(api_key=API_KEY, base_url=BASE_URL) # 原生 SDK,第 3、4 章用
llm = ChatOpenAI(model=MODEL, api_key=API_KEY, # LangChain 封装,第 5、7、8、9 章用
base_url=BASE_URL, temperature=0)
if __name__ == "__main__":
resp = client.chat.completions.create(
model=MODEL, messages=[{"role": "user", "content": "用一句话介绍你自己"}])
print(resp.choices[0].message.content)
跑通 python init_llm.py,后面的章节就畅通无阻了。
3. 先别用框架:手写一个工具调用 Agent
很多人学了半年 LangChain 却不知道 Agent 到底是什么。Agent 的本质就是一个 while 循环:模型要么直接回答,要么返回"我要调工具"的结构化请求,你执行完把结果塞回对话,再问模型——直到它不再要工具为止。这一章我们不用任何框架,50 行代码跑通它。
3.1 Function Calling 的消息循环
user ──► LLM ──► 需要工具? ──是──► 你执行工具 ──► tool 结果回填 messages ──► 再问 LLM
│ │
└──否──► 输出最终回答 ◄──────────────────────────┘
3.2 完整可运行代码
# agent_from_scratch.py
import json
from init_llm import client, MODEL
# ---------- 第一步:写真正的工具函数 ----------
def get_weather(city: str) -> str:
"""查询城市天气(演示用假数据,生产中换成高德/和风天气 API)"""
data = {"北京": "晴,26°C,南风2级", "上海": "小雨,22°C", "深圳": "多云,30°C"}
return data.get(city, f"{city}:暂无数据")
def calculate(expression: str) -> str:
"""计算数学表达式"""
if not set(expression) <= set("0123456789+-*/(). "):
return "非法表达式,只允许数字和四则运算符"
try:
return str(eval(expression))
except Exception as e:
return f"计算失败:{e}"
TOOL_FUNCS = {"get_weather": get_weather, "calculate": calculate}
# ---------- 第二步:用 JSON Schema 向模型描述工具 ----------
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定中国城市当前的天气情况",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如:北京"}
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "计算一个数学四则运算表达式的值",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "如 (3+5)*2"}
},
"required": ["expression"],
},
},
},
]
# ---------- 第三步:Agent 主循环 ----------
def run_agent(user_input: str, messages: list | None = None, max_rounds: int = 10) -> str:
messages = messages if messages is not None else []
messages.append({"role": "user", "content": user_input})
for _ in range(max_rounds):
resp = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
msg = resp.choices[0].message
messages.append(msg.model_dump(exclude_none=True)) # 回填助手消息(含 tool_calls)
# 模型没要工具 → 这就是最终回答
if not msg.tool_calls:
return msg.content
# 模型要调工具 → 逐个执行并回填结果
for tc in msg.tool_calls:
func = TOOL_FUNCS[tc.function.name]
args = json.loads(tc.function.arguments) # 注意:参数是 JSON 字符串!
result = func(**args)
print(f" [调工具] {tc.function.name}{args} → {result}")
messages.append({
"role": "tool",
"tool_call_id": tc.id, # 必须带上调用 id 对应关系
"content": str(result),
})
return "超过最大工具调用轮次,任务终止"
if __name__ == "__main__":
history = []
while True:
q = input("\n你:")
if q in ("exit", "quit"):
break
answer = run_agent(q, history) # history 在循环外 → 天然多轮对话
print(f"助手:{answer}")
运行效果:
你:北京现在天气怎么样?适合出门吗
[调工具] get_weather{'city': '北京'} → 晴,26°C,南风2级
助手:北京现在晴,26°C,南风2级,天气不错,适合出门。早晚略凉,可以带件薄外套。
你:那我打车来回 78 块,门票 120,吃饭预算 200,一共要花多少
[调工具] calculate{'expression': '78+120+200'} → 398
助手:打车 78 + 门票 120 + 吃饭 200,一共 398 元。
3.3 四个必踩的坑(提前告诉你)
- 参数是字符串:
tc.function.arguments是 JSON 字符串,必须json.loads,模型偶尔还会生成非法 JSON,生产代码要加try/except。 - tool 消息必须带
tool_call_id,且顺序与tool_calls一一对应,否则直接报 400。 - 并行工具调用:
tool_calls是列表,模型可能一次要调多个工具,别只处理第一个。 - 死循环:模型可能反复调同一个工具,
max_rounds熔断必须有。
理解了这三节,后面所有框架对你来说都只是"把这段循环封装起来 + 加上状态管理和中间件"而已。
4. RAG:给模型接上知识库
原笔记只在选型表里提了一嘴 Embedding,却没有任何 RAG 实操——这是不小的缺口,因为 RAG 是目前落地率最高的大模型应用形态。核心思想一句话:模型不知道的事,先检索出相关资料,塞进 prompt 里让它"开卷考试"。
4.1 最小流程
离线:文档 → 解析 → 切块(Chunk) → Embedding → 存入向量库
在线:用户问题 → Embedding → 向量库检索 TopK → (Rerank) → 拼进 Prompt → LLM 生成
4.2 最小可运行 RAG(60 行,无需向量数据库)
Embedding 用阿里云百炼的 OpenAI 兼容接口(也可换成硅基流动等任何兼容服务,或本地 BAAI/bge-m3):
# rag_minimal.py
import numpy as np
from openai import OpenAI
emb_client = OpenAI(api_key="sk-换成你的百炼key",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
EMB_MODEL = "text-embedding-v3"
# ---------- 离线:准备知识库 ----------
DOCS = [
"公司年假政策:入职满1年可享5天年假,满3年10天,满10年15天。",
"报销制度:差旅费需在行程结束后15天内提交,单张发票超过500元需部门总监审批。",
"考勤规定:弹性工作制,核心在岗时间为10:00-16:00,每月可申请2次居家办公。",
"试用期规定:试用期3-6个月,试用期工资为转正的80%,入职即缴纳五险一金。",
]
def embed(texts: list[str]) -> np.ndarray:
resp = emb_client.embeddings.create(model=EMB_MODEL, input=texts)
return np.array([d.embedding for d in resp.data])
doc_vecs = embed(DOCS) # 切块+向量化(演示用整句当 chunk)
# ---------- 在线:检索 + 生成 ----------
def retrieve(query: str, top_k: int = 2) -> list[str]:
q = embed([query])[0]
scores = doc_vecs @ q / (np.linalg.norm(doc_vecs, axis=1) * np.linalg.norm(q))
top_idx = scores.argsort()[::-1][:top_k]
return [DOCS[i] for i in top_idx]
def rag_answer(question: str) -> str:
context = "\n".join(f"【资料{i+1}】{c}" for i, c in enumerate(retrieve(question)))
prompt = (
"请严格根据以下资料回答问题,资料里没有的信息就明确说“不知道”,不要编造。\n\n"
f"{context}\n\n问题:{question}"
)
resp = client.chat.completions.create( # 复用第 2 章的生成模型
model=MODEL, messages=[{"role": "user", "content": prompt}])
return resp.choices[0].message.content
if __name__ == "__main__":
print(rag_answer("我入职5年了,有几天年假?"))
print(rag_answer("团建费能报销吗?")) # 资料里没有 → 应回答不知道
文件头部加一句 from init_llm import client, MODEL 即可运行。
4.3 从玩具到生产:五个工程化要点
- 切块策略:按语义/标题层级切,300~500 token + 10% 重叠,别粗暴按固定字符切。
- 文档解析:PDF 表格、扫描件是重灾区,上 Dots.OCR / MinerU 这类解析模型,解析烂=满盘皆输。
- 混合检索:向量检索(语义)+ BM25(关键词)融合,解决"专有名词搜不到"的问题。
- Rerank 精排:召回 50 条 →
bge-reranker精排取 Top5,命中率常提升 10%+。 - 向量库选型:数据量小用 FAISS/Chroma,生产用 Milvus / Qdrant / Elasticsearch。
评估指标盯住两个:召回命中率(正确答案所在的 chunk 是否被检索到)和答案忠实度(是否基于资料、有无幻觉)。
5. MCP:把工具升级成可复用的标准服务
5.1 MCP 到底是什么
第 3 章我们把工具写成 Python 函数,直接塞给模型——这在单项目里没问题,但工具一多、项目一多就会出现:每个项目复制一份工具代码、每个工具一套鉴权逻辑、换框架全得重写。
MCP(Model Context Protocol,Anthropic 于 2024 年底提出的开放协议) 的思路是:把工具、数据、提示词封装成独立的 MCP Server 进程,任何 Agent(任何框架、任何语言)都通过统一协议像"插 USB"一样接入它。
┌─────────────┐ MCP 协议 (stdio / Streamable HTTP) ┌──────────────┐
│ Host 应用 │ ──► MCP Client ──────────────────────────► │ MCP Server A │ (本地工具)
│ (Agent) │ ──► MCP Client ──────────────────────────► │ MCP Server B │ (远程 SaaS)
└─────────────┘ └──────────────┘
- 三种能力:Tools(可执行函数)、Resources(可读取的数据,如文件/数据库记录)、Prompts(预设提示词模板)。
- 两种主流传输:
stdio(本地子进程,最常用)和Streamable HTTP(远程服务,SSE 已被官方标记为废弃,新项目直接用 Streamable HTTP)。 - 生态现状:官方与社区已积累数千个现成 Server(文件系统、Git、数据库、高德地图、12306、Playwright……),接进 Agent 就能用。
5.2 MCP 与 Function Calling 的关系
二者不是替代关系,是分层关系:Function Calling 是模型"决定调用"的能力(模型层),MCP 是工具"如何被标准化供给"的协议(工程层)。MCP Client 会把 Server 的工具列表转换成模型的 tools schema,对模型来说,MCP 工具和本地函数长得一模一样。
5.3 实战:用 FastMCP 写一个自己的 MCP Server
# weather_mcp_server.py
from fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(city: str) -> str:
"""查询指定中国城市当前的天气。
Args:
city: 城市名,如"北京"、"上海"
"""
data = {"北京": "晴,26°C", "上海": "小雨,22°C", "深圳": "多云,30°C"}
return data.get(city, f"{city}:暂无数据")
@mcp.tool()
def plan_trip(origin: str, destination: str) -> str:
"""规划两个城市之间的出行建议。
Args:
origin: 出发城市
destination: 目的城市
"""
return f"{origin} → {destination}:建议高铁出行,全程约4.5小时,二等座553元"
@mcp.resource("config://city-list")
def city_list() -> str:
"""支持查询的城市清单(演示 Resource 能力)"""
return "北京、上海、深圳"
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输;远程部署用 mcp.run(transport="http", port=9000)
注意:docstring 就是工具的 description,模型靠它决定何时调用、怎么传参——写清楚 Args 说明比写花哨的函数名重要得多。
5.4 实战:Agent 通过 MCP Client 接入 Server
用 langchain-mcp-adapters,MCP 工具会被自动转成 LangChain 工具:
# mcp_agent.py
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
from init_llm import llm
async def main():
client = MultiServerMCPClient({
# 本地 stdio Server:就是我们刚写的文件
"weather": {
"transport": "stdio",
"command": "python",
"args": ["weather_mcp_server.py"],
},
# 远程 Server 示例(换成真实地址与鉴权头即可):
# "amap": {
# "transport": "streamable_http",
# "url": "https://your-mcp-gateway/amap/mcp",
# "headers": {"Authorization": "Bearer xxx"},
# },
})
tools = await client.get_tools() # 拉取所有 Server 的全部工具
print("已加载工具:", [t.name for t in tools])
agent = create_agent(
model=llm,
tools=tools,
system_prompt="你是出行助手,可以查天气、规划行程。",
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "北京天气怎么样?我想从北京去上海,给个建议"}]}
)
print(result["messages"][-1].content)
asyncio.run(main())
也可以按 Server 分开拉取:tools = await client.get_tools(server_name="weather"),便于按业务域分给不同 Agent(第 8 章会用到)。
5.5 Elicitation:工具执行到一半,回头向用户补参数
传统工具调用是单向的:Agent 给参数,Server 返回结果,缺参数只能报错重来。MCP 2025-06-18 规范引入的 Elicitation 允许 Server 在执行中途通过 ctx.elicit() 向用户"现场补材料"——带 JSON Schema 的结构化表单,用户填完,工具接着跑:
# profile_server.py
from fastmcp import FastMCP, Context
from pydantic import BaseModel, Field
mcp = FastMCP("profile-server")
class UserDetails(BaseModel):
age: int = Field(description="年龄")
city: str = Field(description="常住城市")
@mcp.tool()
async def create_profile(name: str, ctx: Context) -> str:
"""为用户创建档案,缺信息时会向用户询问"""
result = await ctx.elicit(
message=f"创建 {name} 的档案还需要以下信息:",
response_type=UserDetails,
)
if result.action == "accept" and result.data:
return f"已创建 {name} 的档案:{result.data.age} 岁,来自{result.data.city}"
if result.action == "decline":
return f"用户拒绝提供,已创建 {name} 的最简档案"
return "操作已取消" # action == "cancel"
if __name__ == "__main__":
mcp.run()
三种 action 必须都处理:accept(用户提交)、decline(明确拒绝)、cancel(直接关窗)。⚠️ 注意两点:客户端必须实现 elicitation 回调(客户端不支持时 ctx.elicit() 会抛错,要降级为"直接报错让用户补全");不要用表单模式索要密码、密钥等敏感信息——数据会经过 MCP 层可见。
5.6 MCP 工程化清单
- Schema 明确:参数名、类型、必填、枚举值、docstring 写清楚——这是模型的"使用说明书"。
- 幂等安全:写操作支持幂等键,避免重试造成重复下单/重复退款。
- 权限最小化:查询、预订、支付、退款分级授权,一个 Server 别什么都能干。
- 超时与熔断:外部 API 必须设超时、限流、重试上限。
- 审计日志:记录谁调用、入参摘要、出参摘要、结果。
- 危险操作不自动执行:走 HITL 人工审批(第 9 章)。
6. Skills:给 Agent 发"岗位手册"
6.1 什么是 Agent Skills
2025 年 Anthropic 推出 Agent Skills 规范后,"Skill" 迅速成为与 MCP 并列的 Agent 基础设施概念。一个 Skill 就是一个文件夹:
skills/
├── 数据周报/
│ ├── SKILL.md ← 必须:YAML 元信息 + Markdown 操作手册
│ ├── template.xlsx ← 可选:模板、脚本、参考资料
│ └── examples.md
└── 舆情分析/
└── SKILL.md
SKILL.md 的结构:
---
name: 数据周报
description: 当用户要求把业务数据整理成周报、生成 Excel 汇总或趋势分析时使用本技能
---
# 数据周报制作流程
1. 先调用 query_sales 工具拉取本周数据,异常值(>3σ)单独标注;
2. 按 template.xlsx 的结构生成汇总表,金额保留两位小数;
3. 趋势分析只陈述事实,禁止预测性表述;
4. 输出前自检:合计数 = 明细之和,不成立则重算。
关键在于渐进式披露(Progressive Disclosure),三层加载:
|
层 |
内容 |
何时进入上下文 |
量级 |
|---|---|---|---|
|
1 |
name + description |
永远在系统提示词里 |
每个技能几十 token |
|
2 |
SKILL.md 正文 |
模型判断任务命中该技能时,主动调用 |
几百~几千 token |
|
3 |
附属资源(模板/脚本/示例) |
正文指引、按需读取 |
不限 |
这样挂 100 个技能也不会撑爆上下文——模型平时只看到"技能目录",用到才翻"手册正文"。
6.2 Skills、MCP、Prompt 的分工
|
|
解决什么 |
类比 |
|---|---|---|
|
MCP / 工具 |
"能做什么"——能力的标准化供给 |
工具箱 |
|
Skills |
"该怎么做"——流程、规范、领域经验 |
岗位操作手册 |
|
System Prompt |
"你是谁"——角色与全局约束 |
员工守则 |
一个成熟 Agent = Prompt(守则)+ Skills(手册,按需翻阅)+ MCP 工具(工具箱)。
6.3 实战:60 行给自己的 Agent 实现 Skills 机制
Skills 不是某个厂商的专利,本质是一个设计模式。我们在第 3 章手写 Agent 的基础上实现它:
# skill_loader.py
import re
from pathlib import Path
def scan_skills(root: str = "skills") -> dict:
"""扫描 skills 目录,解析所有 SKILL.md 的元信息与正文"""
skills = {}
for p in Path(root).glob("*/SKILL.md"):
text = p.read_text(encoding="utf-8")
m = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", text, re.S)
if not m:
continue
meta = dict(
line.split(":", 1) for line in
(l.strip() for l in m.group(1).strip().splitlines()) if ":" in line
)
skills[meta["name"].strip()] = {
"description": meta.get("description", "").strip(),
"body": m.group(2).strip(),
"dir": str(p.parent),
}
return skills
SKILLS = scan_skills()
def skills_catalog() -> str:
"""生成注入系统提示词的"技能目录"(第一层)"""
return "\n".join(f"- {name}:{s['description']}" for name, s in SKILLS.items())
def load_skill(name: str) -> str:
"""加载指定技能的完整操作手册(第二层)。当用户任务匹配某个技能时调用。"""
s = SKILLS.get(name)
return s["body"] if s else f"技能 {name} 不存在,可用技能:{list(SKILLS)}"
接入第 3 章的手写 Agent,只需加三处:
# 1) 系统提示词注入技能目录
SYSTEM_PROMPT = (
"你是公司数据助手。当用户任务命中以下技能时,必须先调用 load_skill 加载操作手册,"
"严格按手册流程执行:\n" + skills_catalog()
)
# 2) 注册工具函数
TOOL_FUNCS["load_skill"] = load_skill
# 3) 注册工具 Schema
TOOLS.append({
"type": "function",
"function": {
"name": "load_skill",
"description": "加载指定技能的完整操作手册。当任务匹配技能目录中的某个技能时必须调用。",
"parameters": {
"type": "object",
"properties": {"name": {"type": "string", "description": "技能名称"}},
"required": ["name"],
},
},
})
# 然后在 run_agent 的 messages 开头插入 {"role": "system", "content": SYSTEM_PROMPT} 即可
6.4 Skill 编写规范(写好 description 就成功了一半)
- description 是触发器:必须写清"什么任务该用我",包含用户可能的原话关键词;写得模糊 = 模型永远想不起它。
- 正文是流程不是散文:编号步骤、明确顺序、给出禁止事项和自检标准。
- 一个技能干一件事:粒度对齐"一个岗位的 SOP",别把全公司制度塞进一个 Skill。
- 重资源放附属文件:模板、示例、脚本放技能目录,正文里写"需要时读取 xx 文件",保持第二层精简。
7. 用框架封装:LangChain 1.x / LangGraph
手写循环利于理解,但生产环境你不想自己维护:消息合并、状态持久化、流式输出、中断恢复、中间件……这些框架都做掉了。
7.1 Graph API:把 Agent 流程显式画成图
LangGraph 的理念:把 Agent 执行流程定义为图——节点(干活)、边(固定流转)、条件边(按条件路由)、状态(节点间共享数据,由 reducer 合并)。
# graph_basic.py —— 完整可运行版(原笔记只给了片段)
from langgraph.graph import StateGraph, MessagesState, START, END
from init_llm import llm
def call_model(state: MessagesState):
"""节点函数:接收 state,返回对状态的增量更新"""
response = llm.invoke(state["messages"])
return {"messages": [response]}
graph = (
StateGraph(MessagesState)
.add_node("call_model", call_model)
.add_edge(START, "call_model")
.add_edge("call_model", END)
.compile()
)
result = graph.invoke({"messages": [{"role": "user", "content": "什么是 LangGraph?"}]})
print(result["messages"][-1].content)
关键概念:
StateGraph:图的构建器,泛型参数是状态结构。MessagesState:预置状态,核心字段是messages,源码就一行:
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
- reducer(合并策略):节点返回的是"增量",reducer 决定怎么合并进旧状态:
|
写法 |
语义 |
场景 |
|---|---|---|
|
|
追加消息,按 ID 去重/更新,dict 自动转 Message |
对话消息 |
|
|
列表拼接 |
日志、结果列表 |
|
不指定 reducer |
新值覆盖旧值 |
布尔标记、当前步骤名、计数器 |
|
自定义函数 |
业务自定义合并 |
用户画像、标签去重、保留最近 N 条 |
7.2 create_agent:一行起一个完整 Agent
LangChain 1.0 的 create_agent 内部就是一个带工具循环的图——等价于我们第 3 章手写的循环 + 状态管理 + 中间件挂载点:
# agent_framework.py
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from init_llm import llm
def get_weather(city: str) -> str:
"""查询指定中国城市当前的天气。"""
data = {"北京": "晴,26°C", "上海": "小雨,22°C", "深圳": "多云,30°C"}
return data.get(city, f"{city}:暂无数据")
agent = create_agent(
model=llm,
tools=[get_weather], # 普通函数 + docstring 即可,框架自动生成 Schema
system_prompt="你是天气助手,回答简洁。",
checkpointer=InMemorySaver(), # 状态持久化:实现多轮对话与断点恢复
)
config = {"configurable": {"thread_id": "user-001"}} # 同一 thread_id = 同一会话
r1 = agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样?"}]}, config)
r2 = agent.invoke({"messages": [{"role": "user", "content": "那上海呢?"}]}, config)
print(r2["messages"][-1].content) # 能正确理解"那"指的是同一类问题
checkpointer 是生产必选项:内存版 InMemorySaver 用于开发,生产换 SqliteSaver / PostgresSaver——没有它,第 9 章的人工审批中断恢复无从谈起。
8. 多 Agent 编排:Swarm 与 Supervisor
8.1 封装粒度:按业务域封装,而不是一工具一 Agent
原笔记这点讲得对,值得保留并强调:不要把每个工具都做成 Agent,那会造成 Agent 爆炸、路由成本升高、上下文碎裂。合理粒度是"一个 Agent 负责一个清晰业务域"——工具多没关系,职责要单一,prompt 边界要清楚。
下面用"出行服务平台"案例跑通两种主流编排模式(代码补全了原笔记缺失的所有变量,可直接运行)。
8.2 Swarm:专家接力式(去中心化)
各 Agent 地位平等,通过 transfer_to_* 工具动态交棒,像客服系统里的"转接人工专家":
# swarm_demo.py
from langchain.agents import create_agent
from langgraph_swarm import create_swarm, create_handoff_tool
from langgraph.checkpoint.memory import InMemorySaver
from init_llm import llm
# ---------- 业务域工具 ----------
def get_weather(city: str) -> str:
"""查询城市天气。"""
return {"北京": "晴,26°C", "上海": "小雨,22°C"}.get(city, "暂无数据")
def plan_route(origin: str, destination: str) -> str:
"""规划两地间路线。"""
return f"{origin}→{destination}:建议高铁,约4.5小时"
def query_train(origin: str, destination: str, date: str) -> str:
"""查询火车票。"""
return f"{date} {origin}→{destination}:G103 07:00发车 二等座553元 有余票"
# ---------- 两个业务域 Agent ----------
gaode_assistant = create_agent(
model=llm,
tools=[
get_weather, plan_route,
create_handoff_tool(
agent_name="railway_assistant",
description="当用户需要查询火车票、车次、车站信息时,转交给铁路助手",
),
],
system_prompt="你是地图出行助手,负责天气查询与路线规划。铁路票务问题请转交铁路助手。",
name="gaode_assistant",
)
railway_assistant = create_agent(
model=llm,
tools=[
query_train,
create_handoff_tool(
agent_name="gaode_assistant",
description="当用户需要查天气、地图、路线规划时,转交给地图助手",
),
],
system_prompt="你是铁路 12306 助手,负责查询车次与票务。天气路线问题请转交地图助手。",
name="railway_assistant",
)
# ---------- 组装 Swarm ----------
app = create_swarm(
agents=[gaode_assistant, railway_assistant],
default_active_agent="gaode_assistant", # 默认入口
).compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "trip-001"}}
r = app.invoke(
{"messages": [{"role": "user", "content": "北京天气如何?再帮我查明天去上海的高铁"}]},
config,
)
print(r["messages"][-1].content) # 地图助手答完天气 → 自动转交铁路助手查车次 → 汇总
handoff 的本质不是普通函数返回,而是返回一个图路由指令:
from langgraph.types import Command
return Command(
goto=agent_name, # 下一步去哪个 Agent
update={"messages": state["messages"] + [tool_message]}, # 把"已转接给 xxx"写回消息历史
graph=Command.PARENT, # 控制权交回父图调度层
)
类比:当前 Agent 发现不归自己管,就填一张转工单;工单进系统,下一个 Agent 接着办。
8.3 Supervisor:主管分派式(中心化)
一个主管 Agent 统一分派任务,子 Agent 干完活交回主管,适合流程可控、要审计的场景:
# supervisor_demo.py
from langchain.agents import create_agent
from langgraph_supervisor import create_supervisor
from init_llm import llm
# ---------- 子 Agent:各自领域 + 各自工具 ----------
research_agent = create_agent(
model=llm, tools=[web_search],
system_prompt="你负责网络搜索与资料调研,输出要有信息来源。",
name="research_agent",
)
flight_agent = create_agent(
model=llm, tools=[query_flight, book_flight],
system_prompt="你负责航班查询与预订,预订前必须向用户确认。",
name="flight_booking_agent",
)
hotel_agent = create_agent(
model=llm, tools=[query_hotel, book_hotel],
system_prompt="你负责酒店查询与预订。",
name="hotel_booking_agent",
)
# ---------- 主管:唯一的调度大脑 ----------
app = create_supervisor(
agents=[research_agent, flight_agent, hotel_agent],
model=llm,
prompt=(
"你是差旅平台主管,管理三个智能体:\n"
"- research_agent:网络搜索、资料调研\n"
"- flight_booking_agent:航班查询、预订、改签\n"
"- hotel_booking_agent:酒店查询、预订、修改订单\n"
"处理规则:\n"
"1. 能根据上下文直接回答的咨询、确认类问题,直接回答,不要分派;\n"
"2. 其他情况按类型分派给对应智能体,一次只分派一个任务;\n"
"3. 涉及预订等资金操作,分派前确认用户已明确同意。"
),
).compile()
create_supervisor 会自动为主管生成 transfer_to_<agent> 工具,每个子 Agent 是一个子图。需要更细控制(如前置注入用户信息、限定子 Agent 出口)时,可以退回 Graph API 手工编排:用 create_handoff_tool 造分派工具,再用 StateGraph.add_node(..., destinations=(...)) 显式声明每个节点的可达路由。
8.4 三种模式怎么选
|
模式 |
路由方式 |
优点 |
风险 |
适用 |
|---|---|---|---|---|
|
Swarm |
Agent 间 |
灵活,去中心化 |
可能互相移交死循环(务必设 |
专家接力、客服转接 |
|
Supervisor |
主管统一分派 |
可控、可审计、易加前后置节点 |
主管是瓶颈,路由错影响全局 |
企业流程、强合规 |
|
Graph API |
手工定义节点/边/条件边 |
最灵活 |
代码量大 |
复杂生产流程、精细控制 |
经验:先 Supervisor 跑通业务,遇到瓶颈再下沉到 Graph API;Swarm 适合"对话在专家间流转"的天然场景。
9. 安全护栏 Guardrails
护栏分三层:数据隔离(敏感信息不脱库)、执行隔离(危险操作不自动跑)、发布隔离(灰度与回滚)。
9.1 生命周期挂点
LangChain 1.0 的中间件(middleware)把护栏挂在 Agent 执行链路的各个环节上:
request
→ 输入黑名单 / PII (最外层闸机)
→ before_agent (Agent 启动前:加载记忆、校验输入)
→ before_model (每次调模型前:动态改 prompt、裁剪消息)
→ wrap_model_call (包裹模型调用:重试、降级、换模型)
→ wrap_tool_call (包裹工具调用:审计、限流)
→ after_model (模型返回后:校验输出)
→ after_agent (Agent 结束后:合规审核、落库)
→ result
典型场景:防 PII 泄露、阻断提示注入、拦截不当内容、执行业务合规规则、验证输出质量。
9.2 确定性护栏 vs 模型驱动护栏
|
类型 |
实现 |
优点 |
缺点 |
|---|---|---|---|
|
确定性护栏 |
正则、关键词、Schema、黑白名单 |
快、便宜、可预测 |
漏语义变体 |
|
模型驱动护栏 |
用 LLM/分类器做语义评估 |
能识别细微问题 |
慢、贵、需治理阈值 |
工程建议:闸机先拦(确定性)、安检员复核(模型驱动)、危险品进人工(HITL)。最外层黑名单示例:
BANNED_KEYWORDS = ["暴力", "攻击", "毒品"]
# 命中策略分级:mask(打码) / block(阻断) / redact(删除) / log_only(仅记录)
注意多义词误杀("黑客攻击技术"可能是编程课)和变体攻击(拼音、谐音、拆字),黑名单只做兜底,不做唯一防线。
9.3 PII Middleware:敏感信息自动脱敏
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
agent = create_agent(
model=llm,
tools=[query_user_info, send_email],
middleware=[
# 内置类型:email / credit_card / ip / mac_address / url
PIIMiddleware("email", strategy="redact", apply_to_input=True),
# 自定义正则:中国手机号
PIIMiddleware(
"phone_number",
detector=r"1[3-9]\d{9}",
strategy="mask", # 138****5678
apply_to_input=True,
apply_to_output=True, # 模型输出也脱敏
apply_to_tool_results=True, # 工具返回的用户资料也脱敏
),
],
)
要点:strategy 支持 redact(替换为类型标签)/ mask(部分打码)/ hash(哈希)/ block(直接阻断);三个 apply_to_* 控制处理时机——对外发信、查库返回用户资料的场景,输出和工具结果务必纳入脱敏。
9.4 Human-in-the-loop:危险操作先审批
适用场景:删改数据、资金交易、对外通知、权限变更。
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
agent = create_agent(
model=llm,
tools=[read_data, execute_sql, write_file],
checkpointer=InMemorySaver(), # HITL 必须配 checkpointer,生产换持久化实现
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True, # 简化写法:中断,允许 approve/edit/reject
"execute_sql": {
"allowed_decisions": ["approve", "reject"],
"description": "即将执行 SQL 写操作,请审批",
},
"read_data": False, # 只读操作自动放行
},
description_prefix="工具执行待审批",
),
],
)
config = {"configurable": {"thread_id": "ops-001"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "把昨天下线的商品从库存表删掉"}]},
config,
)
# 命中中断:result["__interrupt__"] 里有待审批的工具调用详情
# 人工决策后,用 Command(resume=...) 从断点继续:
result = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}), # 或 edit / reject
config,
)
四种决策:
|
决策 |
含义 |
|---|---|
|
|
原样执行 |
|
|
人工修改参数后执行 |
|
|
拒绝并把原因反馈给模型 |
|
|
跳过工具,直接人工回复 |
生产环境必须用持久化 checkpointer(SqliteSaver/PostgresSaver),否则进程一重启,中断的现场就丢了。
9.5 after_agent:输出合规审核(改进版)
医疗场景:最终回复不得包含医疗建议或承诺;金融场景:不得含投资建议、收益承诺。原笔记的示例会直接覆盖原消息且无任何留痕,这里给出改进版:
from langchain.agents.middleware import after_agent
from init_llm import llm
@after_agent
def content_output_review(state, runtime):
"""Agent 结束后做合规审核:不覆盖原消息,审核结论写入 metadata 留痕"""
last_msg = state["messages"][-1]
if last_msg.type != "ai":
return None
review_prompt = (
"你是合规审核员。审核标准:回复中不能包含任何医疗建议、诊断结论或疗效承诺。\n"
"只回复 JSON:{\"pass\": true/false, \"reason\": \"...\"}\n\n"
f"待审核内容:\n{last_msg.content}"
)
review = llm.invoke([{"role": "user", "content": review_prompt}])
last_msg.metadata["review"] = review.content # 留痕,不覆盖
if '"pass": false' in review.content.replace(" ", ""):
return {"messages": [{
"role": "ai",
"content": "抱歉,该问题涉及医疗建议,请咨询专业医师。我可以为您介绍相关健康常识。",
}]}
return None
浙公网安备 33010602011771号