跳到主要内容

Agent:在引擎之上手写的「LLM ↔ 工具」循环

本章讲 Haystack 的 agentic 那一半。关键认知:Agent 不是用 Pipeline 图实现的,而是一个 @component 内部手写的 while 循环——它本身也是一个组件,能被塞进更大的管道。

1. Agent 是什么

Agentcomponents/agents/agent.py:104)是「一个会用工具的 LLM」。run(messages) 后它会反复

┌────────────────────────────────────────────────┐
│ 1. 把当前消息历史发给 chat_generator(LLM) │◀────┐
│ 2. LLM 回复里有 tool_call 吗? │ │
│ 没有 → 退出(默认 exit_conditions=["text"]) │ │
│ 有 → 3. ToolInvoker 执行这些工具 │ │
│ 4. 把工具结果作为消息回灌进历史 │─────┘
│ 命中 exit_conditions 的工具 → 退出 │
└────────────────────────────────────────────────┘

没工具时,Agent 就退化成一个普通 ChatGenerator(构造时会 warn,agent.py:333-337)。

2. 三个零件

零件角色在哪
chat_generator调 LLM,必须支持 tools 参数构造时校验,agent.py:262-268
ToolInvoker真正执行 LLM 想调的工具components/tools/tool_invoker.py:82
State跨步骤累积的「记忆」(消息历史 + 自定义键)components/agents/state/state.py:82

3. 核心机制:循环主体

Agent.runagent.py:726)的 while exe_context.counter < self.max_agent_stepsagent.py:809):

第一步——问 LLMagent.py:816-827):注意它复用了管道引擎的单组件执行器 Pipeline._run_component,而不是自己直接调 chat_generator.run。这样 tracing、错误包装、断点逻辑全都共享。回复存进 state 的 messagesagent.py:858)。

退出判断agent.py:860-872):当没有 ToolInvoker,或者「最后一条消息是非空的 assistant 文本且没有任何 tool_call」时,退出。注释强调要求最后一条是非空 assistant 文本,免得一个空回复就误触发退出(agent.py:860-863)。

第二步——执行工具agent.py:895-909):同样走 Pipeline._run_componentToolInvoker,输入是 LLM 那几条带 tool_call 的消息 + 当前 state。工具结果消息回灌进 state(agent.py:941-943)。

显式退出条件agent.py:945-948):如果 exit_conditions 不是默认的 ["text"],而是某些工具名,_check_exit_conditionsagent.py:1209)检查 LLM 是否调了这些工具且没出错——是则退出。注释点明它检查每个 tool_call,所以并行工具调用的顺序无关紧要(agent.py:1213-1215)。

4. State:Agent 的记忆

Statestate.py:82)是一个带 schema 的键值容器。每个键有一个 type 和一个合并 handler

  • list 类型默认用 merge_lists(追加),state.py:139
  • 其他类型默认用 replace_values(覆盖),state.py:141

这解释了为什么消息能「累积」:messages 键是 list[ChatMessage] + merge_listsagent.py:289),每轮 state.set("messages", 新消息)追加而非覆盖。而 Agent 在替换整段历史时(确认策略改写后)会显式传 handler_override=replace_values 来覆盖(agent.py:893)。

Agent 的输入/输出口是动态的:__init__ 里遍历 state_schema,每个键都 component.set_input_type / 汇总进 set_output_typesagent.py:304-311)——这正是第 2 章 §2.4 动态 socket 的实战。所以工具可以通过 inputs_from_state / outputs_to_state 读写这些状态键。

5. ToolInvoker:并行执行工具

ToolInvoker.runtool_invoker.py:566):

  1. 只保留带 tool_call 的消息(tool_invoker.py:626);
  2. 准备每个工具调用的参数,包括从 State 注入参数(_inject_state_args);
  3. 用线程池并行执行所有工具调用(tool_invoker.py:649ThreadPoolExecutor);
  4. 逐个收结果:出错时若 raise_on_failure=False 就把错误包成一条 tool 消息回给 LLM(tool_invoker.py:659-664),否则抛;成功则把输出 merge 进 State 并造一条 tool 结果消息(tool_invoker.py:665-674)。

「错误变成消息回灌给 LLM」是个关键设计:让模型自己看到工具报错、决定要不要重试或换路(raise_on_tool_invocation_failure 默认 False,agent.py:329)。

Tool 本身(tools/tool.py:19)是个 dataclass:name / description / parameters(JSON schema) / functioninvoketool.py:261)就是 self.function(**kwargs),包一层 ToolInvocationError工具函数必须是同步的——__post_init__ 显式拒绝 async(tool.py:105-110)。

6. 断点与恢复

Agent 支持在 chat_generator 或某个工具处下断点(AgentBreakpoint / ToolBreakpoint),触发时把整个执行上下文存成 AgentSnapshot,之后能从断点恢复_initialize_from_snapshotagent.py:645)。恢复时若断在工具处,会跳过下一次 LLM 调用(skip_chat_generatoragent.py:677,811)。这套断点机制和管道的 pipeline_snapshot(第 1 章)共享底层(core/pipeline/breakpoint.py),是 Haystack 面向「生产可调试」的体现。


下一章:巧妙之处、边界局限、横向对比和完整代码地图。见 05-clever-and-boundaries.md