跳转到主要内容

Documentation Index

Fetch the complete documentation index at: https://qitor.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

这是整条课程里第一个完整的智能体。 它故意保持小巧,但并不玩具化。这一课里你会看到一个真正可运行的编码智能体,它具备完整的组件链:
  • 强类型 state
  • 真实的大模型适配层
  • 真实的系统提示词
  • 真实的解析器(parser)
  • 真实的工具
  • 真实的 reduce()(归约,将观测结果与决策归约回状态)循环
  • 真实的 qita 追踪记录
你会结合 examples/patterns/react.py 来学习,但这节课本身会把设计解释清楚,不要求你先反向阅读源码。

你要构建什么

任务非常小:
  • 打开 buggy_module.py
  • 修复 add(a, b),让它返回 a + b
  • 运行验证命令
正因为任务小,它特别适合拿来观察完整 QitOS 执行内核,而不会被额外编排噪音掩盖。

本课的设计选择

设计分支本课选择为什么这是最好的起点
任务形态单文件缺陷修复 + 单个验证命令易验证、易追踪
Statescratchpadtarget_filetest_command足够影响下一步决策,但不引入冗余状态
模型适配层返回文本的 OpenAICompatibleModel简单、可迁移、与供应商无关
提示词契约REACT_SYSTEM_PROMPT明确的一次一工具文本协议
解析器ReActTextParserThought: / Action: 完整对应
工具手工 ToolRegistry + 紧凑 CodingToolSet先学清楚工具界面,再学预设
记忆不使用运行很短,没有引入单独记忆的必要
历史使用 Engine 默认行为在理解内核之前不引入额外上下文控制
追踪能力qita board从第一课就学会检查内核

为什么从文本 ReAct 适配层开始

本课使用:
OpenAICompatibleModel(...)
以及:
model_parser=ReActTextParser()
这样你能看到最透明的一条路径: messages -> text model output -> ReAct parser -> Decision -> tool execution 其中解析器负责将原始模型输出解析为决策(智能体每步的结构化决策),而协议(模型输出的格式契约)定义了输出的格式约定。我们不从原生工具调用、XML、JSON 或模型专属适配层开始,因为那些选择会在你还没理解核心循环之前,过早引入协议耦合。

系统提示词是协议,不是装饰

本课使用的 ReAct 提示词大致是:
You are a reliable ReAct agent.

Rules:
- Use at most one tool call per response.
- Never invent tool names or arguments.
- If a tool result is enough to conclude, output final answer directly.

Output contract (strict):
Thought: <one concise reasoning sentence>
Action: <tool_name>(arg=value, ...)
or
Final Answer: <final answer only>
在代码里通常表现为:
def build_system_prompt(self, state: ReactState) -> str | None:
    return render_prompt(
        REACT_SYSTEM_PROMPT,
        {"tool_schema": self.tool_registry.get_tool_descriptions()},
    )
这一点很关键。ReActTextParser 不是在猜模型输出,它是在消费这份明确约定好的协议。 本课最重要的第一条经验就是:
  • 提示词格式与解析器选择,本质上是一项联合设计决策
  • 改了其中一个,通常就要连另一个一起改

本课的模型适配层

示例里的模型构造通常长这样:
def build_model() -> OpenAICompatibleModel:
    return OpenAICompatibleModel(
        model=MODEL_NAME,
        api_key=api_key,
        base_url=MODEL_BASE_URL,
        temperature=0.2,
        max_tokens=2048,
    )
为什么这里选这个适配层:
  • 它适用于 OpenAI 兼容端点
  • 模型返回纯文本,便于观察
  • 与基于提示词注入工具模式的路径天然兼容
  • 让课程可以在不同供应商与本地网关间迁移
这节课你不是在选择最强模型,而是在选择最能暴露内核结构的适配层。
1

围绕下一步决策来设计 state

本课的 state 故意很小:
@dataclass
class ReactState(StateSchema):
    scratchpad: list[str] = field(default_factory=list)
    target_file: str = "buggy_module.py"
    test_command: str = (
        'python -c "import buggy_module; assert buggy_module.add(20, 22) == 42"'
    )
为什么是这三个字段:
  • scratchpad:保存压缩后的最近轨迹,供下一步决策使用
  • target_file:让智能体始终围绕单一工件行动
  • test_command:把任务完成变成一个可执行成功条件
这是第一条 QitOS 习惯:只给 state 加那些会改变未来决策的字段。
2

暴露尽量小的工具界面

示例使用手工注册表,让你能精确看到暴露给模型的能力:
registry = ToolRegistry()
registry.include(
    CodingToolSet(
        workspace_root=workspace_root,
        include_notebook=False,
        enable_lsp=False,
        enable_tasks=False,
        enable_web=False,
        expose_modern_names=False,
    )
)
CodingToolSet 本身是捆绑包,但它的界面仍是你主动裁剪出来的。对第 1 课来说,最合适的工具集合只需要覆盖:
  • 查看文件
  • 编辑文件
  • 运行验证命令
在任务没有提出需求之前,不要暴露更大的工具面。
3

把提示词与解析器明确绑在一起

智能体构造函数里通常会这样写:
super().__init__(
    tool_registry=registry,
    llm=llm,
    model_parser=ReActTextParser(),
)
要把这句话读成一个整体:这个智能体要求模型输出 ReAct 文本,并由 ReAct 解析器负责解析。在 QitOS 里,这个绑定本身就是适配层的一部分。
4

prepare 只组织下一步真正需要的上下文

prepare() 负责把 state 变成提示词就绪的文本:
def prepare(self, state: ReactState) -> str:
    lines = [
        f"Task: {state.task}",
        f"Target file: {state.target_file}",
        f"Verification command: {state.test_command}",
        f"Step: {state.current_step}/{state.max_steps}",
    ]
    if state.scratchpad:
        lines.append("Recent trajectory:")
        lines.extend(state.scratchpad[-8:])
    return "\n".join(lines)
第二条核心习惯就是:prepare() 不是 state 的全量转储,而是提示词视图。
5

让归约决定智能体记住什么

ReAct 的学习发生在 reduce() 里:
def reduce(
    self,
    state: ReactState,
    observation: dict[str, Any],
    decision: Decision[Action],
) -> ReactState:
    action_results = (
        observation.get("action_results", [])
        if isinstance(observation, dict)
        else []
    )
    if decision.rationale:
        state.scratchpad.append(f"Thought: {decision.rationale}")
    if decision.actions:
        state.scratchpad.append(f"Action: {format_action(decision.actions[0])}")
    if action_results:
        first = action_results[0]
        state.scratchpad.append(f"Observation: {first}")
        if isinstance(first, dict) and int(first.get("returncode", 1)) == 0:
            state.final_result = "Patch applied and verification passed."
    state.scratchpad = state.scratchpad[-30:]
    return state
这一个函数里藏着三条原则:
  • 不是每条观测结果(每步后智能体接收的结构化观察结果)都值得回灌进未来上下文
  • state 是压缩工作记忆,而不是全量日志
  • final_result 是成功的显式信号
6

理解本课还没有引入什么

第 1 课不使用:
  • decide() 覆写
  • 显式规划
  • 记忆适配器
  • 自定义历史
  • 上下文压缩(compaction)
  • 模型专属协议覆写
不是因为 QitOS 没有这些能力,而是因为在你看清默认主路径之前,过早引入反而会模糊边界。
7

运行示例,并用 qita 检查内核

运行:
python examples/patterns/react.py
然后查看:
qita board --logdir runs
qita 中重点检查:
  • 发送给模型的提示词是否符合预期
  • 解析器是否干净地产生了 ThoughtAction
  • 工具输出是否让成功条件变得明确
  • final_result 是否在真正成功的第一时间被设置

为什么这一课不引入记忆或上下文压缩

本课最正确的记忆选择就是不用。 原因很简单:
  • 运行很短
  • scratchpad 已足够容纳有效上下文
  • 过早引入检索或上下文压缩会模糊架构边界
在 QitOS 中,记忆不是高级智能体的标配,它是为真实长时运行问题准备的答案。

完整示例

完整可运行代码位于:

第 2 课会引入什么

第 2 课会保持相同的模型适配层和执行解析器,但引入一个新的设计点: 规划应该变成显式状态与显式控制边界,而不是藏在更长的思考里。

下一课:PlanAct

加入规划器、游标与 decide() 门控,但不更换核心运行时

相关参考:Kit

回看本课使用的 ReActTextParser、提示词模板与编码工具界面