FunctionAgent

用户问「比较两篇里 GAPDH 的实验对象」,一次 query() 只会检索一次。需要模型自己决定:要不要搜、搜什么、证据不够是否再搜。FunctionAgent 把函数或 QueryEngineTool 交给带 native function calling 的 LLM,在「选工具 → 执行 → 再想」之间打转,直到它认为可以回答。

段末注释FunctionAgent 是预构建的工具循环工作流;默认两次 run 之间无记忆,多轮必须传入同一个 Context

LLM 选 paper_search,QueryEngine 回 source_nodes,再决定结束或再转一圈(科普示意)


1. 一句话定位

维度 内容
角色 知识层之上的短工具环:何时调用检索/计算由模型决定
输入 → 输出 user_msg: str → 终态文本(工具中间结果在事件流里)
典型调用入口 FunctionAgent(...).run()QueryEngineTool.from_defaults()Context(agent)
与 LangChain / LangGraph 近邻是 create_agentcheckpoint / interrupt / 持久化线程仍用 LangGraph

出现背景:QueryEngine 是 DAG。Agent 把「再检索」从你写的 if 变成模型的 tool_calls。本地小模型 tool calling 不稳时,改 ReActAgent(纯文本 ReAct),不要假装 FunctionAgent 万能。


2. 前置依赖与环境

1
2
3
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
ollama pull qwen3.5:9b
ollama pull nomic-embed-text
  • 必须设 Settings.embed_model(工具里的 QueryEngine 要检索)与 Agent 的 llm
  • runasync;脚本里 asyncio.run
  • 生产服务用 await agent.run(...),不要在事件循环里再 asyncio.run

3. 实现逻辑

1
2
3
4
5
6
7
8
1. 建 Index / QueryEngine(DAG 检索+合成)
2. QueryEngineTool.from_defaults(qe, name, description)
3. FunctionAgent(tools=[tool, 普通函数...], llm, system_prompt)
4. await agent.run(user_msg)
5. 内部:messages + tools schema → LLM
6. 若 tool_calls:执行工具(QE.query / 函数)→ 把结果写回消息 → 回到 5
7. 无 tool_calls:把最终文本当 Stop
8. 多轮:ctx = Context(agent);每次 run(..., ctx=ctx)

字段级变形

1
2
3
4
user_msg = "实验对象是什么物种?"
→ tool_calls: paper_search(query="实验对象 物种 GAPDH")
→ QueryEngine.query → Response + source_nodes
→ 模型读 str(Response) 写最终句

工具 description 决定会不会被选中,和 Router 的 description 同一类契约。


4. 原理说明

主轴是:Agent 循环改的是消息列表;QueryEngine 每次被调用都是一次独立 DAG。

1
2
3
4
5
6
7
8
9
10
1. run(user_msg) 若未传 ctx,内部新建空 Context
2. 把 system_prompt、历史、user_msg、tools JSON schema 发给 llm
3. 模型返回 content 和/或 tool_calls(name, arguments, id)
4. 按 name 找到 Python 函数或 QueryEngineTool
5. QueryEngineTool 调用 query_engine.query(arguments["query"] 或等价字段)
6. 工具返回值变成下一条 tool 消息;QE 的 source_nodes 默认被压成字符串,**不一定**原样出现在终态对象上
7. 再次调 LLM;直到无 tool_calls 或触达步数/timeout
8. 同一 Context 的第二次 run 才能看见第 1 轮消息
少了第 8 步 → 「我刚告诉你物种」下一句就忘
少了 description → 模型从不调检索,用参数记忆胡编

QueryEngineTool.from_defaults 出现在步骤 2。name 须合法函数名;description 写清检索范围。return_direct=True 时工具输出直接当终态,不再让模型改写(引用格式可能更稳、也更生硬)。

FunctionAgent 出现在步骤 3。tools 可混:裸函数(靠 docstring + 类型注解)与 Tool 对象。

Context 出现在步骤 8。它是 Workflow 的运行时状态,不是 LangGraph checkpointer:进程退出即失。

步数失控、要审批、要落盘:把本 Agent 当 LangGraph 一个节点,或直接在图里调 QE,不要在 FunctionAgent 里模拟 interrupt。


5. 最小可运行示例

1
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import asyncio

from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.tools import QueryEngineTool
from llama_index.core.workflow import Context
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(model="qwen3.5:9b", request_timeout=120.0, temperature=0)
Settings.embed_model = OllamaEmbedding(
model_name="nomic-embed-text",
base_url="http://localhost:11434",
)

index = VectorStoreIndex.from_documents(
[Document(text="定量 PCR 以小鼠肝脏 GAPDH 为内参。实验对象为 C57BL/6 小鼠。",
doc_id="paper_001", metadata={"section": "methods"})],
transformations=[SentenceSplitter(chunk_size=128, chunk_overlap=20)],
)
qe = index.as_query_engine(similarity_top_k=3)

search = QueryEngineTool.from_defaults(
query_engine=qe,
name="paper_search",
description="检索本地方法学笔记,回答物种、内参、剂量。不要用于闲聊。",
)

agent = FunctionAgent(
tools=[search],
llm=Settings.llm,
system_prompt="只根据工具返回的资料回答;没有证据就说不知道。",
)

async def main():
ctx = Context(agent)
# 输入
r1 = await agent.run("实验对象是什么物种?", ctx=ctx)
print(r1)
r2 = await agent.run("上一问的内参基因是什么?", ctx=ctx)
print(r2)
# 预期形态:两轮都围绕小鼠 / GAPDH;第二轮依赖 ctx 才接得上

asyncio.run(main())

无 native tools 的模型:

1
2
from llama_index.core.agent.workflow import ReActAgent
agent = ReActAgent(tools=[search], llm=Settings.llm, system_prompt="...")

6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
tools list 可调用函数/Tool 空列表则纯聊天,检索不会发生 1~5 个起步 description 必须互斥、可执行
system_prompt str 角色与拒答策略 不写「无证据则不知道」易编造 短约束 + 工具职责 与 tool description 分工
llm LLM 负责 tool_calls 不支持 function calling 则空转或乱参 明确支持 tools 的模型 失败改 ReActAgent
ctx Context,可选 跨 run 的消息/状态 不传 = 每轮失忆 多轮会话必传 生产要持久化请换 LangGraph
return_direct bool,Tool 上默认 False 工具输出是否跳过模型改写 True 保真引用、少一次 LLM 强引用场景 True FAQ 可开,比较题关掉
streaming bool,Agent 上 是否流式事件 部分本地模型需 False 先 False 跑通 前端再开
timeout Workflow 秒数 整段循环上限 过短误杀再检索;过长空转烧钱 60~180 与工具 HTTP 超时分开设

7. 适用 / 不适用

维度 适用 不适用
任务形态 要不要检索不确定、可能二次检索 固定「每次都检索一次」——QueryEngine 更便宜可测
集成约束 LLM 支持 tool calling 高风险动作要人批——LangGraph interrupt
工程阶段 原型多工具 进程重启续跑——checkpointer,不是 Context

8. 易踩坑

  1. 两次 run 不传 Context:多轮断裂。
  2. tool description 写成「很有用」:从不检索或乱检索。
  3. 终态字符串里找不到 source_nodes:模型改写时丢掉;需要引用就 return_direct 或自己在工具里格式化 citation。
  4. 用 FunctionAgent 模拟审批:没有 thread_id 级恢复。

小结

  • FunctionAgent = 短工具环;QueryEngine 仍是被调用的 DAG
  • 多轮必须 同一 Context
  • 引用要在 Tool 层钉死,不要指望终态文本自动带 source_nodes
  • 环要持久化、要 HITL → LangGraph。

参考链接

-------------本文结束感谢您的阅读-------------