为纪念 35 周年:《Terminator 2: Judgment Day》将在全球影院重映
2026-07-13
2026-07-14 0
第 04 篇主要演示了 Tools。Resources 和 Tools 的分工需要先说清楚:

复制代码Tools → LLM 主动执行的操作(动词)
LLM 决定什么时候调用,调用会产生副作用
示例:create_issue、update_statusResources → LLM 可读取的数据(名词)
Host 决定何时注入到上下文,只读,无副作用
示例:当前 Sprint 状态、项目统计
"读取某个状态"用 Resource,"执行某个操作"用 Tool。同一个数据可以两种都实现:get_issue 作为 Tool(LLM 控制何时调用),jira://issue/PROJ-101 作为 Resource(Host 在需要时自动注入)。
Static Resource 每次返回相同的数据(如项目列表)。Dynamic Resource 每次读取返回当前最新状态,内容随时间变化。
Sprint 状态资源:每次读取都是实时数据
复制代码_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)
测试结果:
复制代码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 就知道何时需要重读:
复制代码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 这是动态数据,必要时重新读取
),
)
一种 Resource 类型对应多个具体实例时,用参数化 URI——list_resources() 枚举所有实例,read_resource() 用一个 handler 处理所有。
每个项目有自己的统计资源:
复制代码@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)
测试结果:
复制代码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 设计原则:
复制代码jira://project/{key}/stats ← 分层结构,类似 REST 路径
jira://sprint/current ← 当前活跃项,不用 ID
jira://dashboard ← 聚合视图,固定 URI不推荐:
jira://stats_PROJ ← 扁平化,不可扩展
jira://data?project=PROJ ← 查询参数,解析复杂
Prompt 模板不一定是静态文本。根据参数值渲染不同章节,让同一个 Prompt 适配多种场景。
事故报告:P0 比 P1 多出 Escalation 章节
复制代码@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## Escalationn"
"- Engineering VP: must be notified within 30 minutesn"
"- SLA breach risk: this may breach the 4-hour P0 SLAn"
) # 条件章节:有 workaround 才包含
workaround_section = ""
if workaround:
workaround_section = f"n## Workaroundn{workaround}n" template = (
f"Create a formal incident report for {issue_key}...n"
f"## Summaryn...n"
f"## Root Causen..."
f"{p0_section}" # ← 条件插入
f"{workaround_section}" # ← 条件插入
) return GetPromptResult(messages=[PromptMessage(role="user",
content=TextContent(type="text", text=template))])
测试结果:
复制代码P0: escalation_section= workaround_section=
P1: escalation_section= workaround_section=
P0 事故触发升级协议(告知 VP、SLA 风险),P1 展示临时绕行方案。LLM 收到不同的模板,生成的报告结构也不同,不需要用一份大而全的模板让 LLM 自己判断哪些章节适用。
标准 Prompt 只有一条用户消息。多轮 Prompt 可以预填一段对话历史,引导 LLM 先执行某步骤,再完成最终输出。
PR 描述:3 轮消息,第 2 轮是 assistant "思考"步骤
复制代码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}nn"
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."
))),
]
)
测试结果:
复制代码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 的用途:
当 Prompt 模板内嵌了"去读某个 Resource / 调用某个 Tool"的指令时,用户不需要提供数据——模板告诉 LLM 如何自己获取:
复制代码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}.nn"
f"Steps:n"
f"1. Read jira://sprint/current to see overall sprint healthn" # ← Resource 引用
f"2. Use search_issues to find issues {team_member} worked onn" # ← Tool 引用
f"3. Write standup:n"
f" Yesterday / Today / Blockers / Sprint health"
))
)])
测试结果:
复制代码References resource: (jira://sprint/current 在模板里)
References tool: (search_issues 在模板里)
LLM 收到这个 Prompt 后会先读 jira://sprint/current,再调用 search_issues,最后生成 standup。用户只需要告诉系统"帮我生成今天的 standup",不需要自己先汇总数据。
Resource 和 Tool 不是非此即彼,可以在 Prompt 里组合引用:
复制代码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 主动调用,可能有副作用)。两类能力各司其职。
Resources
jira://project/{key}/stats),不用查询参数Prompts
欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页