上一课的 AI 只能"空口分类"——它没有你公司的任何数据。

第 4 课:知识库与自定义工具


为什么需要自定义工具?

上一课的 AI 只能"空口分类"——它没有你公司的任何数据。

但真正的客服需要: - 查知识库找答案 - 查工单数据库看历史 - 查订单系统核实信息 - 更新工单状态

这些都需要自定义工具。Claude Agent SDK 用 MCP(Model Context Protocol)让你给 AI 接上自己的数据库和 API。

MCP 工具的核心概念

code
AI 想查知识库
  │
  ├── AI: "我需要调用 search_faq 工具"
  │
  ├── SDK: 调用你写的 search_faq 函数
  │
  ├── 你的函数: 查数据库,返回结果
  │
  └── AI: 拿到结果,组织回复给客户

AI 自己决定"什么时候"用工具,你负责写"工具能干什么"。

第一个工具:知识库检索

先准备一个简单的 FAQ 知识库(data/faq.json):

code
[
  {
    "id": "faq-001",
    "question": "如何重置密码",
    "answer": "进入登录页面,点击"忘记密码",输入注册邮箱,我们会发送重置链接到您的邮箱。链接有效期 24 小时。",
    "category": "account",
    "keywords": ["密码", "重置", "忘记", "登录不了"]
  },
  {
    "id": "faq-002",
    "question": "如何升级到企业版",
    "answer": "登录后台,进入"设置-套餐管理",选择企业版,按提示完成支付。企业版支持年付(8折优惠)和月付。如需定制方案,请联系销售团队。",
    "category": "billing",
    "keywords": ["企业版", "升级", "套餐", "价格"]
  },
  {
    "id": "faq-003",
    "question": "如何导出数据",
    "answer": "进入"数据管理"页面,选择要导出的数据范围和格式(CSV/Excel),点击"导出"。大量数据导出可能需要几分钟,导出完成后会发邮件通知。",
    "category": "feature",
    "keywords": ["导出", "下载", "数据", "Excel", "CSV"]
  },
  {
    "id": "faq-004",
    "question": "退款政策",
    "answer": "购买后 7 天内可无理由全额退款。超过 7 天但未满 30 天,可退还未使用月份的费用。请联系客服提交退款申请。",
    "category": "billing",
    "keywords": ["退款", "退钱", "取消", "不想用了"]
  },
  {
    "id": "faq-005",
    "question": "API 调用限制",
    "answer": "免费版:100次/天,基础版:10000次/天,企业版:无限制。超出限制会返回 429 错误。可在后台查看当前用量。",
    "category": "technical",
    "keywords": ["API", "限制", "429", "频率", "调用次数"]
  }
]

现在写检索工具:

code
"""知识库检索工具"""

import json
from pathlib import Path
from claude_agent_sdk import tool, create_sdk_mcp_server


# 加载知识库
FAQ_DATA = json.loads(Path("data/faq.json").read_text(encoding="utf-8"))


@tool(
    "search_faq",
    "在知识库中搜索与客户问题相关的 FAQ 条目",
    {"keyword": {"type": "string", "description": "搜索关键词"}}
)
async def search_faq(keyword: str) -> str:
    """关键词搜索 FAQ"""
    results = []
    keyword_lower = keyword.lower()

    for faq in FAQ_DATA:
        # 在问题、答案、关键词中搜索
        searchable = (
            faq["question"] + faq["answer"] + " ".join(faq["keywords"])
        ).lower()

        if keyword_lower in searchable:
            results.append(faq)

    if not results:
        return f"没有找到与 '{keyword}' 相关的 FAQ。"

    output = f"找到 {len(results)} 条相关 FAQ:\n\n"
    for faq in results:
        output += f"📋 {faq['question']}\n"
        output += f"   {faq['answer']}\n\n"

    return output

@tool 装饰器详解

code
@tool(
    "search_faq",                     # 工具名(AI 看到的名字)
    "在知识库中搜索相关的 FAQ 条目",      # 工具描述(AI 根据这个决定要不要用)
    {                                  # 参数定义
        "keyword": {
            "type": "string",
            "description": "搜索关键词",
        }
    }
)
async def search_faq(keyword: str) -> str:
    # 函数体:实际的搜索逻辑
    ...

AI 会看到: - 名字:search_faq - 描述:搜索 FAQ - 参数:需要一个 keyword

然后 AI 自己决定什么时候调用这个工具。

第二个工具:工单数据库

code
"""工单数据库工具"""

import json
import datetime
from claude_agent_sdk import tool

# 模拟工单数据库
TICKET_DB: dict[str, dict] = {
    "TK-001": {
        "id": "TK-001",
        "customer": "张三",
        "email": "zhangsan@example.com",
        "subject": "登录页面一直转圈",
        "status": "open",
        "priority": "high",
        "created_at": "2025-03-15 10:30:00",
        "history": ["客户提交工单", "已分配给技术团队"],
    },
    "TK-002": {
        "id": "TK-002",
        "customer": "李四",
        "email": "lisi@example.com",
        "subject": "企业版咨询",
        "status": "pending",
        "priority": "low",
        "created_at": "2025-03-15 11:00:00",
        "history": ["客户提交工单"],
    },
}


@tool(
    "get_ticket",
    "根据工单号查询工单详情",
    {"ticket_id": {"type": "string", "description": "工单号,如 TK-001"}}
)
async def get_ticket(ticket_id: str) -> str:
    """查询工单"""
    ticket = TICKET_DB.get(ticket_id.upper())
    if not ticket:
        return f"未找到工单 {ticket_id}"
    return json.dumps(ticket, ensure_ascii=False, indent=2)


@tool(
    "update_ticket_status",
    "更新工单状态(open/pending/resolved/closed)",
    {
        "ticket_id": {"type": "string", "description": "工单号"},
        "status": {"type": "string", "description": "新状态: open/pending/resolved/closed"},
        "note": {"type": "string", "description": "操作备注"},
    }
)
async def update_ticket_status(ticket_id: str, status: str, note: str = "") -> str:
    """更新工单状态"""
    ticket = TICKET_DB.get(ticket_id.upper())
    if not ticket:
        return f"未找到工单 {ticket_id}"

    valid_statuses = ["open", "pending", "resolved", "closed"]
    if status not in valid_statuses:
        return f"无效状态: {status},可选: {valid_statuses}"

    old_status = ticket["status"]
    ticket["status"] = status
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    ticket["history"].append(f"[{timestamp}] {old_status}{status}: {note}")

    return f"✅ 工单 {ticket_id} 状态已更新: {old_status}{status}"


@tool(
    "search_tickets_by_customer",
    "根据客户名字或邮箱搜索工单",
    {"query": {"type": "string", "description": "客户名字或邮箱"}}
)
async def search_tickets_by_customer(query: str) -> str:
    """搜索客户的工单"""
    results = []
    query_lower = query.lower()

    for ticket in TICKET_DB.values():
        if (query_lower in ticket["customer"].lower()
                or query_lower in ticket["email"].lower()):
            results.append(ticket)

    if not results:
        return f"未找到客户 '{query}' 的工单"

    output = f"找到 {len(results)} 个工单:\n"
    for t in results:
        output += f"  {t['id']}: {t['subject']} [{t['status']}]\n"
    return output

组装工具:创建 MCP 服务器

code
from claude_agent_sdk import create_sdk_mcp_server

# 创建 MCP 服务器,把所有工具注册进去
support_tools = create_sdk_mcp_server(
    name="support-tools",
    version="1.0.0",
    tools=[
        search_faq,
        get_ticket,
        update_ticket_status,
        search_tickets_by_customer,
    ],
)

完整示例:带工具的智能客服

code
import anyio
from claude_agent_sdk import (
    query, ClaudeAgentOptions,
    AssistantMessage, ResultMessage, TextBlock, ToolUseBlock,
    tool, create_sdk_mcp_server,
)

# === 工具定义(简化版)===

FAQ_DATA = [
    {"question": "如何重置密码", "answer": "进入登录页,点忘记密码,输入邮箱即可。", "keywords": ["密码", "重置"]},
    {"question": "退款政策", "answer": "7天内无理由退款,超7天退未使用部分。", "keywords": ["退款", "退钱"]},
    {"question": "API 限制", "answer": "免费100次/天,基础1万/天,企业无限。", "keywords": ["API", "限制", "429"]},
]

@tool("search_faq", "搜索常见问题知识库", {"keyword": {"type": "string", "description": "关键词"}})
async def search_faq(keyword: str) -> str:
    results = [f for f in FAQ_DATA if any(k in keyword for k in f["keywords"])]
    if not results:
        return f"知识库中没有找到 '{keyword}' 相关内容。"
    return "\n".join(f"Q: {f['question']}\nA: {f['answer']}" for f in results)

TICKETS = {"TK-001": {"id": "TK-001", "customer": "张三", "subject": "登录问题", "status": "open"}}

@tool("get_ticket", "查询工单详情", {"ticket_id": {"type": "string", "description": "工单号"}})
async def get_ticket(ticket_id: str) -> str:
    import json
    t = TICKETS.get(ticket_id.upper())
    return json.dumps(t, ensure_ascii=False) if t else f"未找到 {ticket_id}"

# === 创建 MCP 服务器 ===
support_server = create_sdk_mcp_server(
    name="support",
    version="1.0.0",
    tools=[search_faq, get_ticket],
)

# === 主程序 ===
SYSTEM_PROMPT = """你是智能客服助手。你有以下工具可以使用:

1. search_faq - 搜索知识库回答常见问题
2. get_ticket - 查询工单详情

工作流程:
1. 理解客户问题
2. 如果是常见问题,先搜知识库
3. 如果涉及特定工单,查询工单详情
4. 给客户专业、友好的回复

用中文回复。"""

async def handle_customer(question: str):
    options = ClaudeAgentOptions(
        system_prompt=SYSTEM_PROMPT,
        mcp_servers={"support": support_server},
        allowed_tools=[
            "mcp__support__search_faq",
            "mcp__support__get_ticket",
        ],
        max_turns=5,
    )

    print(f"\n客户: {question}")
    async for msg in query(prompt=question, options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    print(f"客服: {block.text}")
                elif isinstance(block, ToolUseBlock):
                    print(f"  🔧 [查询 {block.name}]")
        elif isinstance(msg, ResultMessage):
            if msg.total_cost_usd and msg.total_cost_usd > 0:
                print(f"  💰 ${msg.total_cost_usd:.4f}")

async def main():
    await handle_customer("我的密码忘了怎么办?")
    print("-" * 40)
    await handle_customer("帮我查一下工单 TK-001 是什么情况?")
    print("-" * 40)
    await handle_customer("你们的 API 有调用次数限制吗?")

anyio.run(main)

运行后你会看到 AI 自动决定什么时候搜知识库、什么时候查工单:

code
客户: 我的密码忘了怎么办?
  🔧 [查询 search_faq]
客服: 您好!重置密码很简单:进入登录页,点击"忘记密码"...

客户: 帮我查一下工单 TK-001 是什么情况?
  🔧 [查询 get_ticket]
客服: 我查到了您的工单 TK-001,当前状态是"处理中"...

工具命名规则

MCP 工具在 allowed_tools 中的命名规则:

code
mcp__{服务器名}__{工具名}

比如: - 服务器名: support,工具名: search_faqmcp__support__search_faq - 服务器名: orders,工具名: get_ordermcp__orders__get_order

工具设计最佳实践

1. 描述要写给 AI 看

code
# ❌ 描述太技术化
@tool("search_faq", "执行 FAQ 表的全文检索查询", ...)

# ✅ 描述像在和 AI 说话
@tool("search_faq", "当客户问常见问题时,在知识库中搜索答案", ...)

2. 返回值要信息丰富

code
# ❌ 返回太少
return "找到了"

# ✅ 返回有用信息
return f"找到 3 条相关 FAQ:\n1. {faq1}\n2. {faq2}\n3. {faq3}"

3. 错误信息要友好

code
# ❌ 抛异常
raise ValueError("Not found")

# ✅ 返回友好错误
return "未找到该工单,请确认工单号是否正确(格式:TK-XXXX)"

本课小结

  • MCP 自定义工具让 AI 能访问你的数据和系统
  • @tool 装饰器定义工具,create_sdk_mcp_server 注册
  • AI 自动决定什么时候用什么工具
  • 工具描述要写给 AI 看,返回值要信息丰富

课后练习

  1. 给知识库加 5 条新的 FAQ 条目
  2. 写一个 create_ticket 工具,让 AI 能创建新工单
  3. 写一个 get_order 工具,查询订单信息(模拟数据即可)

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录