MCP 系列(05):Resources 和 Prompts 进阶——动态数据、参数化 URI 与多轮模板

Resources 和 Tools 的本质区别

第 04 篇主要演示了 Tools。Resources 和 Tools 的分工需要先说清楚:

markdown 复制代码
Tools     → LLM 主动执行的操作(动词)
            LLM 决定什么时候调用,调用会产生副作用
            示例:create_issue、update_status

Resources → LLM 可读取的数据(名词)
            Host 决定何时注入到上下文,只读,无副作用
            示例:当前 Sprint 状态、项目统计

"读取某个状态"用 Resource,"执行某个操作"用 Tool。同一个数据可以两种都实现:get_issue 作为 Tool(LLM 控制何时调用),jira://issue/PROJ-101 作为 Resource(Host 在需要时自动注入)。


模式 1:动态 Resource

Static Resource 每次返回相同的数据(如项目列表)。Dynamic Resource 每次读取返回当前最新状态,内容随时间变化。

Sprint 状态资源:每次读取都是实时数据

python 复制代码
_sprint_progress_pct = 65  # 模拟随时间推进的进度

@server.read_resource()  # type: ignore[arg-type]
async def read_resource(uri: str) -> str:
    if str(uri) == "jira://sprint/current":
        global _sprint_progress_pct
        _sprint_progress_pct = min(100, _sprint_progress_pct + random.randint(0, 3))

        return json.dumps({
            "sprint_name": "Sprint 42",
            "progress_pct": _sprint_progress_pct,            # ← 每次不同
            "last_updated": datetime.now(timezone.utc).isoformat(),  # ← 时间戳变化
            "days_remaining": 5,
            "p0_open": count_p0_open(),                      # ← 随 issue 状态变化
        }, indent=2)

测试结果:

ini 复制代码
Read 1: progress=65%  last_updated=...62+00:00
Read 2: progress=67%  last_updated=...04+00:00
→ ✓ data changed between reads

Dynamic Resource 让 LLM 始终读到最新数据,适合监控类信息(Sprint 健康、队列深度、服务状态)。把这类数据硬编码在 Prompt 里,LLM 会用过时的数据做决策。

Resource 描述里标注动态性,LLM 就知道何时需要重读:

python 复制代码
Resource(
    uri="jira://sprint/current",
    name="Current Sprint Status",
    description=(
        "Live status of the active sprint: progress, issue counts. "
        "Read this when the user asks about sprint health or velocity. "
        "Re-read if you need up-to-date data --- content changes over time."
        # ↑ 明确告知 LLM 这是动态数据,必要时重新读取
    ),
)

模式 2:参数化 URI

一种 Resource 类型对应多个具体实例时,用参数化 URI------list_resources() 枚举所有实例,read_resource() 用一个 handler 处理所有。

每个项目有自己的统计资源:

python 复制代码
@server.list_resources()
async def list_resources() -> list[Resource]:
    resources = []
    for key, proj in PROJECTS.items():
        resources.append(Resource(
            uri=f"jira://project/{key}/stats",  # type: ignore[arg-type]
            name=f"{proj['name']} Stats",
            description=f"Issue statistics for {proj['name']} ({key}).",
        ))
    return resources

@server.read_resource()  # type: ignore[arg-type]
async def read_resource(uri: str) -> str:
    if str(uri).startswith("jira://project/") and str(uri).endswith("/stats"):
        # 从 URI 解析出项目 key
        proj_key = str(uri).split("/")[3].upper()  # jira://project/{key}/stats

        if proj_key not in PROJECTS:
            raise ValueError(f"Unknown project: {proj_key}")

        proj_issues = [i for i in ISSUES.values() if i["project"] == proj_key]
        return json.dumps({
            "project": proj_key,
            "total": len(proj_issues),
            "by_status": count_by(proj_issues, "status"),
            "by_priority": count_by(proj_issues, "priority"),
        }, indent=2)

测试结果:

bash 复制代码
jira://project/PROJ/stats   → total=3, by_status={'Open': 2, 'In Progress': 1}
jira://project/MOBILE/stats → total=1, by_status={'Open': 1}
jira://project/INFRA/stats  → total=1, by_status={'Done': 1}

好处: LLM 可以按需读取某个项目的统计,不需要一次拉取所有数据。Host 也可以根据用户当前的 context(在哪个项目工作)自动注入对应的 Resource,而不是每次注入所有项目数据。

URI scheme 设计原则:

bash 复制代码
jira://project/{key}/stats      ← 分层结构,类似 REST 路径
jira://sprint/current           ← 当前活跃项,不用 ID
jira://dashboard                ← 聚合视图,固定 URI

不推荐:
jira://stats_PROJ               ← 扁平化,不可扩展
jira://data?project=PROJ        ← 查询参数,解析复杂

模式 3:条件 Prompt(按参数渲染不同章节)

Prompt 模板不一定是静态文本。根据参数值渲染不同章节,让同一个 Prompt 适配多种场景。

事故报告:P0 比 P1 多出 Escalation 章节

python 复制代码
@server.get_prompt()
async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:
    if name == "incident_report":
        severity = args.get("severity", "P1").upper()
        workaround = args.get("workaround", "")

        # 条件章节:只有 P0 才包含升级处理
        p0_section = ""
        if severity == "P0":
            p0_section = (
                "\n## Escalation\n"
                "- Engineering VP: must be notified within 30 minutes\n"
                "- SLA breach risk: this may breach the 4-hour P0 SLA\n"
            )

        # 条件章节:有 workaround 才包含
        workaround_section = ""
        if workaround:
            workaround_section = f"\n## Workaround\n{workaround}\n"

        template = (
            f"Create a formal incident report for {issue_key}...\n"
            f"## Summary\n...\n"
            f"## Root Cause\n..."
            f"{p0_section}"         # ← 条件插入
            f"{workaround_section}" # ← 条件插入
        )

        return GetPromptResult(messages=[PromptMessage(role="user",
            content=TextContent(type="text", text=template))])

测试结果:

ini 复制代码
P0: escalation_section=✓  workaround_section=✗
P1: escalation_section=✗  workaround_section=✓

P0 事故触发升级协议(告知 VP、SLA 风险),P1 展示临时绕行方案。LLM 收到不同的模板,生成的报告结构也不同,不需要用一份大而全的模板让 LLM 自己判断哪些章节适用。


模式 4:多轮 Prompt

标准 Prompt 只有一条用户消息。多轮 Prompt 可以预填一段对话历史,引导 LLM 先执行某步骤,再完成最终输出。

PR 描述:3 轮消息,第 2 轮是 assistant "思考"步骤

python 复制代码
if name == "pr_description":
    return GetPromptResult(
        messages=[
            # Turn 1: 用户请求
            PromptMessage(role="user", content=TextContent(type="text", text=(
                f"You are a senior engineer writing a PR description.\n"
                f"PR addresses: {issue_key}\n\n"
                f"First, use get_issue to read the Jira issue details."
            ))),
            # Turn 2: assistant 思考步骤(预填,引导结构化输出)
            PromptMessage(role="assistant", content=TextContent(type="text", text=(
                "I'll fetch the issue details and then write a PR description "
                "with: title, motivation, changes summary, test plan, and links."
            ))),
            # Turn 3: 最终指令
            PromptMessage(role="user", content=TextContent(type="text", text=(
                "Now write the complete PR description in Markdown."
            ))),
        ]
    )

测试结果:

less 复制代码
Turn count: 3
Turn 1 (user):      You are a senior engineer writing a PR description...
Turn 2 (assistant): I'll fetch the issue details and then write a PR description...
Turn 3 (user):      Now write the complete PR description in Markdown.

多轮 Prompt 的用途:

  • 预填 assistant 消息:植入"思考大纲",引导 LLM 输出特定格式
  • 模拟 Few-shot:预填示例对话,让 LLM 理解期望的输出风格
  • 链式任务:第 1 轮要求 LLM 先获取数据,第 2 轮要求处理数据

模式 5:无必填参数的 Prompt(模板引用 Resource 和 Tool)

当 Prompt 模板内嵌了"去读某个 Resource / 调用某个 Tool"的指令时,用户不需要提供数据------模板告诉 LLM 如何自己获取:

python 复制代码
if name == "standup_update":
    team_member = args.get("team_member", "")
    return GetPromptResult(messages=[PromptMessage(role="user",
        content=TextContent(type="text", text=(
            f"Generate a daily standup update for {team_member}.\n\n"
            f"Steps:\n"
            f"1. Read jira://sprint/current to see overall sprint health\n"  # ← Resource 引用
            f"2. Use search_issues to find issues {team_member} worked on\n"  # ← Tool 引用
            f"3. Write standup:\n"
            f"   Yesterday / Today / Blockers / Sprint health"
        ))
    )])

测试结果:

less 复制代码
References resource: ✓  (jira://sprint/current 在模板里)
References tool:     ✓  (search_issues 在模板里)

LLM 收到这个 Prompt 后会先读 jira://sprint/current,再调用 search_issues,最后生成 standup。用户只需要告诉系统"帮我生成今天的 standup",不需要自己先汇总数据。


Resource 与 Tool 的协作

Resource 和 Tool 不是非此即彼,可以在 Prompt 里组合引用:

ini 复制代码
LLM 执行 standup_update Prompt 的完整流程:

1. 读 Resource   jira://sprint/current → 了解 Sprint 整体健康
2. 调 Tool       search_issues(query="closed", assignee="alice") → 昨天完成的
3. 调 Tool       search_issues(query="open", assignee="alice") → 今天要做的
4. 生成输出      综合以上数据生成 standup 格式

Resource 提供背景上下文(主动注入,无副作用),Tool 执行查询或操作(LLM 主动调用,可能有副作用)。两类能力各司其职。


设计 Checklist

Resources

  • 动态数据在描述里标注"content changes over time",提示 LLM 何时重读
  • 参数化 URI 用分层路径(jira://project/{key}/stats),不用查询参数
  • 聚合资源(Dashboard)适合一次给 LLM 大量上下文,减少多次工具调用

Prompts

  • 条件章节用 Python 字符串拼接控制,不在模板里留空章节
  • 多轮 Prompt 的 assistant 消息只写"思考大纲",不写实际结论(留给 LLM 填充)
  • 无必填参数的 Prompt 在模板里明确指示 LLM 去调用哪个 Resource/Tool

参考资料


欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页

相关推荐
冬奇Lab1 小时前
开源项目第158期:cangjie-skill — 把书、视频、播客里的方法论蒸馏成可调用的 AI Skills
人工智能·开源·资讯
习明然2 小时前
我的本地化AI项目(三)
人工智能·python·electron·c#·avalonia
程序猿炎义3 小时前
一人内容团队——用Amazon Quick Desktop实现小红书从选题到发布的全流程自动化
大数据·人工智能·microsoft·自动化·小红书
阿虎儿4 小时前
daytona创建snapshot: Failed to get initial runner: Error: No available runners
人工智能
KaneLogger4 小时前
防止 AI Agent 误删文件,最简单的办法就是加一个 Hook
llm·agent·trae
字节跳动视频云技术团队4 小时前
火山引擎 × 央视网 打造 2026 世界杯沉浸式观赛盛宴
人工智能·音视频开发
IT_陈寒4 小时前
Java线程池这个坑我算是踩明白了
前端·人工智能·后端