PromptTask 智能体循环:提示词组装与 ReAct 子任务
30 秒导读: 在 Griptape 里,让 LLM「反复调用工具直到给出答案」的那台发动机,就是一个
PromptTask。本章讲清它两件核心工作:(1) 每次调 LLM 前,如何把系统提示、用户输入、历史工具回合、对话记忆拼成一份完整上下文(prompt_stack);(2) 拿到 LLM 的回话后,如何用ActionsSubtask把它解析成「思考 + 一组动作」,并行执行工具,再把结果回灌 LLM——循环往复,直到 LLM 说出Answer。
本章聚焦「单个 Task 内部的智能体循环」。谁来调度多个 Task(Agent / Pipeline / Workflow)见 01-structures-and-task-graph.md;LLM 请求怎么真正发出去见 03-drivers-and-provider-neutrality.md;工具本身的 schema 与分发见 04-tools-and-activities.md。
1. 这是什么(零基础也能懂)
-
一句话定义:
PromptTask是「一次让 LLM 干活」的最小单元。给它一段输入和一堆工具,它会自己盘旋——问 LLM、执行工具、把结果告诉 LLM、再问——直到 LLM 给出最终答案。 -
解决什么问题: 光调一次 LLM 常常不够。比如「北京现在几点、比纽约早几小时」,LLM 得先调一个时钟工具、拿到结果、再算差值才能回答。这种「想一步、做一步、看结果、再想」的来回,就是 ReAct(Reason + Act,推理与行动交替)。
PromptTask把这套来回封装好,你只管给输入和工具。 -
它能做什么:
- 把 system 模板 + 用户输入 + 历史动作 + 对话记忆拼成一次调用的完整上下文。
- 解析 LLM 的回话,认出里面要调哪些工具、传什么参数。
- 并行执行多个工具动作,校验参数是否符合工具的 schema。
- 把工具输出回灌给 LLM,循环到出答案;并用
max_subtasks兜底防死循环。
-
用起来什么样: 下面是最小用法,一个装了工具的 Agent 内部就是一个
PromptTask。
# 示意,非源码 —— 展示 PromptTask 在做什么
from griptape.tasks import PromptTask
from griptape.tools import DateTimeTool
task = PromptTask(
input="现在几点?",
tools=[DateTimeTool()], # 给它「手脚」
)
result = task.run() # 内部:问 LLM → 调 DateTimeTool → 把时间回灌 LLM → 出答案
print(result.value)
- 一句话直觉: 把
PromptTask想成一个「带便签本的助手」。便签本(prompt_stack)每一轮都重抄一遍:最上面是岗位职责(system),然后是你的问题,然后是「我上一步调了什么工具、得到什么」的流水账。每次它都拿着这整本便签去问 LLM,所以 LLM 永远看得到全部来龙去脉。
本节不碰底层。目标:你现在知道「PromptTask = 一个会自己反复调工具的循环」。
2. 顶层全景(一次 run 大概怎么转)
PromptTask.run() 继承自 BaseTask.run(),它只是个骨架:before_run → try_run → after_run,任何异常都兜成 ErrorArtifact,最后状态置 FINISHED(base_task.py:170-188)。真正的智能体循环在 try_run 里。
部件一句话职责:
| 部件 | 干什么 | 在哪里 |
|---|---|---|
prompt_stack(属性) | 每轮把 system+输入+历史子任务+记忆拼成一次调用的完整上下文 | prompt_task.py:125-146 |
try_run | 循环入口:先跑一次 LLM,再依次过 subtask_runners 管线 | prompt_task.py:208-221 |
default_run_actions_subtasks | 工具循环本体:解析动作 → 执行 → 回灌 LLM → 再来 | prompt_task.py:300-325 |
ActionsSubtask | ReAct 引擎:解析 LLM 文本/结构、绑定工具、并行执行 | actions_subtask.py 整个类 |
prompt_driver | 真正把 prompt_stack 发给某个 LLM provider | 见 03 章 |
主线走一遍(高层,不进代码):
输入 ──► try_run
│
├─(1) prompt_driver.run(prompt_stack) # 先问一次 LLM,得初步回话
│
└─(2) 依次过 subtask_runners 管线:
├─ default_run_actions_subtasks # 工具循环(本章重点)
└─ default_run_output_schema_validation_subtasks # 若设了 output_schema,逼 LLM 产出合规结构
│
└──► 最终 Answer / 结构化输出 / ErrorArtifact
注意 (1) 和 (2) 里每次「问 LLM」用的都是同一个 prompt_stack 属性——而它是动态计算的:每次访问都重新遍历当前 self.subtasks,把已经跑过的工具回合重新拼进去。这就是「回灌」得以发生的机制,下一节详解。
3. 核心机制一:prompt_stack —— 每轮重新组装的完整上下文
-
它要解决的小问题: LLM 是无状态的——你这 次调用没告诉它的,它就不知道。所以每一轮都必须把「岗位职责 + 原始问题 + 到目前为止做过什么」完整地重新递给它。
-
思路:
prompt_stack写成一个 property 而非缓存字段。每次读它,都按固定顺序现拼一份新的PromptStack(一串带角色的消息)。顺序是精心设计的。
装配顺序(prompt_task.py:126-146):
PromptStack(tools=..., output_schema=...) # 带上工具清单与期望输出 schema
│
├─ ① add_system_message(system_template) # 岗位职责:规则 + 动作格式 + 工具名单
├─ ② [conversation_memory 插在这里] # 见下方「记忆插位」
├─ ③ add_user_message(self.input) # 原始用户输入(经 Jinja 渲染)
└─ ④ 若还没有最终 output:
for s in self.subtasks: # 把每个历史子任务的「动作+结果」回放进来
s.add_to_prompt_stack(stack)
记忆插位是个巧点(prompt_task.py:142-144):对话记忆不是简单追加到末尾,而是「有 system 就插在 index 1(紧跟 system 之后),否则插在 index 0」。这样长期记忆永远紧贴岗位职责、排在当轮用户输入之前,符合 LLM 对「背景知识在前、当前任务在后」的偏好。
真实代码,注意 ③④ 的分叉:
# prompt_task.py:134-141 —— 有最终输出就回放输出,否则回放所有子任务
stack.add_user_message(self.input)
if self.output:
stack.add_assistant_message(self.output.to_text())
else:
for s in self.subtasks:
s.add_to_prompt_stack(stack)
- 关键细节:
system_template由default_generate_system_template(prompt_task.py:231-243)用system.j2渲染,里面塞进了 rulesets、动作 JSON schema、工具名单,以及两个开关文本:use_native_tools和reflect_on_tool_use(决定要不要教 LLM 用Thought:/Actions:/<|Response|>的 ReAct 文本格式)。- 因为
④每轮都重新遍历self.subtasks,所以「上一轮工具的输出」是通过子任务列表增长 + prompt_stack 重算回灌给 LLM 的——不是手工拼字符串。这是整个循环的关节。
4. 核心机制二:try_run 与 subtask_runners 管线
-
它要解决的小问题: 一次 LLM 回话可能是「最终答案」,也可能是「我要调工具」,还可能需要「产出的结构不合规、得重来」。得有一条流水线把这些情况依次处理掉。
-
思路:
try_run先无条件问一次 LLM 拿初步输出,然后把这个输出依次喂给subtask_runners列表里的每个处理器,每个处理器要么原样透传、要么把它加工成新输出。
真实代码,循环的主干:
# prompt_task.py:213-221
output = self.prompt_driver.run(self.prompt_stack).to_artifact(
meta={"is_react_prompt": not self.prompt_driver.use_native_tools}
)
for subtask_runner in self.subtask_runners:
output = subtask_runner(output)
默认两个 runner(prompt_task.py:81-87):
| runner | 职责 | 触发条件 |
|---|---|---|
default_run_actions_subtasks | 跑工具循环(ReAct 本体) | 有 tools 才干活,否则原样透传 |
default_run_output_schema_validation_subtasks | 逼 LLM 产出符合 output_schema 的结构 | 设了 output_schema 才干活 |
那个 meta={"is_react_prompt": ...} 标记很关键:它告诉下游的 ActionsSubtask——这段输出到底是「ReAct 文本」(要正则解析)还是「native 结构化动作」(直接读对象)。开关就是 prompt_driver.use_native_tools。
4.1 工具循环:default_run_actions_subtasks
这 是本章的心脏。逐行看它怎么盘旋(prompt_task.py:300-325):
# prompt_task.py:300-325(节选)
if not self.tools:
return subtask_input # 没工具,直接透传
subtask = self.add_subtask(ActionsSubtask(subtask_input, ...)) # 解析首轮回话
while subtask.output is None: # output 还没定 = 这轮是动作,不是答案
if len(self.subtasks) >= self.max_subtasks:
subtask.output = ErrorArtifact(f"Exceeded tool limit ...") # 兜底防死循环
else:
subtask.run() # 并行执行本轮的工具动作
if self.reflect_on_tool_use:
output = self.prompt_driver.run(self.prompt_stack).to_artifact(...) # 把结果回灌 LLM
subtask = self.add_subtask(ActionsSubtask(output)) # 解析下一轮回话
return subtask.output
理解这个循环的钥匙:subtask.output is None 就代表「LLM 还想调工具」。
ActionsSubtask一被add_subtask,就在attach_to里立刻解析输入。如果解析出的是Answer(没有动作),它当场把output置为答案 →while条件为假 → 循环退出,返回答案。- 如果解析出的是一组动作,
output保持None→ 进循环体:subtask.run()执行工具(把output填成工具结果),然后若reflect_on_tool_use,再问一次 LLM(此时prompt_stack已把刚跑完的动作+结果回放进去),拿到的新回话又包成新ActionsSubtask赋回subtask,进入下一圈判断。
两个开关改变循环形状:
| 开关 | True(默认) | False |
|---|---|---|
reflect_on_tool_use | 执行工具后再回灌 LLM,让它看结果决定下一步——真正的多轮 ReAct | 执行完工具就返回原始工具输出,不给 LLM 复盘 |
max_subtasks(默认 20) | 子任务数达上限即塞 ErrorArtifact 退出 | ——(数值上限,不是布尔) |
default_run_output_schema_validation_subtasks(prompt_task.py:327-344)结构几乎一样,只是把「执行工具」换成「用 OutputSchemaValidationSubtask 校验结构,不合格就再问 LLM 重产」,同样受 max_subtasks 兜底。
4.2 一张回合制循环图(LLM ↔ 工具)
怎么读: 上半是 LLM 的地盘,下半是工具的地盘。中间那条竖线是 prompt_stack —— 每轮工具跑完,结果都被写回它,LLM 下一轮就看得见。命中 Answer 即跳出。
┌──────────────────────── prompt_stack(每轮重算)────────────────────────┐
│ system 职责 + 用户输入 + [历史: 动作①→结果① / 动作②→结果② …] + 记忆 │
└───────────────────────────────────┬───────────────────────────────────┘
│ 递给 LLM
▼
┌──────────────────┐
┌───────────►│ 问 LLM 一轮 │
│ │ prompt_driver.run │
│ └────────┬─────────┘
│ │ 回话包成 ActionsSubtask 解析
│ ▼
│ ┌──────────────────┐ 有 Answer,无动作
│ │ 是动作 还是 答案? │────────────────────► 返回最终答案 ✔
│ └────────┬─────────┘
│ │ 是一组动作(output is None)
│ ▼
│ ┌──────────────────┐
│ │ 并行执行工具动作 │ run_actions(futures)
│ │ (04 章讲 dispatch)│
│ └────────┬─────────┘
│ │ 结果写回子任务列表 → prompt_stack
reflect_on_tool_use=True │
└─────────────────────┘
(reflect=False:不回灌,直接返回工具输出)
兜底:len(subtasks) ≥ max_subtasks(默认 20)→ 塞 ErrorArtifact 强制跳出