大模型快速入门,基顾入门编

总体路线图

一个能上线的大模型 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 魔搭

国内访问稳定,国产模型齐全,pip install modelscope

Hugging Face Hub

全球最大开源模型社区,国内可用镜像 hf-mirror.com

企业内部制品库

生产推荐:版本固化、灰度、审计、离线分发

# 方式一: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 下载前检查清单

镜像下载务必校验文件大小与 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 四个必踩的坑(提前告诉你)

  1. 参数是字符串:tc.function.arguments 是 JSON 字符串,必须 json.loads,模型偶尔还会生成非法 JSON,生产代码要加 try/except。
  2. tool 消息必须带 tool_call_id,且顺序与 tool_calls 一一对应,否则直接报 400。
  3. 并行工具调用:tool_calls 是列表,模型可能一次要调多个工具,别只处理第一个。
  4. 死循环:模型可能反复调同一个工具,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 从玩具到生产:五个工程化要点

  1. 切块策略:按语义/标题层级切,300~500 token + 10% 重叠,别粗暴按固定字符切。
  2. 文档解析:PDF 表格、扫描件是重灾区,上 Dots.OCR / MinerU 这类解析模型,解析烂=满盘皆输。
  3. 混合检索:向量检索(语义)+ BM25(关键词)融合,解决"专有名词搜不到"的问题。
  4. Rerank 精排:召回 50 条 → bge-reranker 精排取 Top5,命中率常提升 10%+。
  5. 向量库选型:数据量小用 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 正文

模型判断任务命中该技能时,主动调用 load_skill 加载

几百~几千 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 决定怎么合并进旧状态:

写法

语义

场景

add_messages

追加消息,按 ID 去重/更新,dict 自动转 Message

对话消息

operator.add

列表拼接

日志、结果列表

不指定 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 间 transfer_to_* 动态交棒

灵活,去中心化

可能互相移交死循环(务必设 max_rounds/recursion_limit)

专家接力、客服转接

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,
)

四种决策:

决策

含义

approve

原样执行

edit

人工修改参数后执行

reject

拒绝并把原因反馈给模型

respond

跳过工具,直接人工回复

生产环境必须用持久化 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

 

posted @ 2026-09-19 17:17  鬼门元歌  阅读(14)  评论(0)    收藏  举报