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
参考资料
- MCP Resources 规范
- MCP Prompts 规范
- 本系列完整 Demo 代码:mcp-05-advanced
欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页