mirror of
https://github.com/tvytlx/ai-agent-deep-dive.git
synced 2026-04-03 07:34:50 +08:00
Improve agent documentation for teaching
This commit is contained in:
390
src/agt/agent.py
390
src/agt/agent.py
@@ -1,12 +1,52 @@
|
|||||||
|
"""
|
||||||
|
教学版 Agent 核心文件。
|
||||||
|
|
||||||
|
本文件的目标不是追求最复杂、最完整的工程实现,而是:
|
||||||
|
|
||||||
|
1. 用尽量清晰的 Python 代码,演示一个 Agent 的核心结构
|
||||||
|
2. 保持代码易读、易改、易扩展,方便教学与逐步演进
|
||||||
|
3. 为后续接入真实 LLM、工具系统、Skills、记忆系统提供稳定骨架
|
||||||
|
4. 在写法上尽量遵循清晰封装、低耦合、易测试的最佳实践
|
||||||
|
|
||||||
|
教学约定:
|
||||||
|
- 所有文档与注释优先使用中文
|
||||||
|
- 类、函数、重要属性都尽量提供清晰文档
|
||||||
|
- 优先追求“容易理解”,再追求“功能丰富”
|
||||||
|
- 允许用最小可用实现来表达结构,但要保留明确扩展点
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, Iterator, Protocol
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Message:
|
class Message:
|
||||||
|
"""
|
||||||
|
表示 Agent 运行时中的一条消息。
|
||||||
|
|
||||||
|
这是一种教学用的最小消息模型。真实产品中,消息通常会更复杂,
|
||||||
|
可能还会包含:消息 ID、父子关系、时间戳、工具块、token 使用信息等。
|
||||||
|
|
||||||
|
属性:
|
||||||
|
role:
|
||||||
|
消息角色。
|
||||||
|
常见值包括:
|
||||||
|
- "user":用户消息
|
||||||
|
- "assistant":模型回复
|
||||||
|
- "tool_result":工具执行结果
|
||||||
|
|
||||||
|
content:
|
||||||
|
消息正文内容。
|
||||||
|
这里为了教学简化为纯文本。
|
||||||
|
|
||||||
|
meta:
|
||||||
|
附加元数据。
|
||||||
|
用于保存额外的结构化信息,例如 turn 编号、chunks、工具名等。
|
||||||
|
"""
|
||||||
|
|
||||||
role: str
|
role: str
|
||||||
content: str
|
content: str
|
||||||
meta: dict[str, Any] = field(default_factory=dict)
|
meta: dict[str, Any] = field(default_factory=dict)
|
||||||
@@ -14,41 +54,306 @@ class Message:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ToolResult:
|
class ToolResult:
|
||||||
|
"""
|
||||||
|
表示工具执行后的结果。
|
||||||
|
|
||||||
|
属性:
|
||||||
|
ok:
|
||||||
|
表示工具执行是否成功。
|
||||||
|
|
||||||
|
content:
|
||||||
|
工具返回给 Agent 的文本内容。
|
||||||
|
在真实系统中,这里也可以扩展为结构化内容。
|
||||||
|
|
||||||
|
meta:
|
||||||
|
工具执行附带的元数据。
|
||||||
|
例如执行耗时、文件路径、命中条数等。
|
||||||
|
"""
|
||||||
|
|
||||||
ok: bool
|
ok: bool
|
||||||
content: str
|
content: str
|
||||||
meta: dict[str, Any] = field(default_factory=dict)
|
meta: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class Tool:
|
class Tool:
|
||||||
def __init__(self, name: str, description: str, handler: Callable[[dict[str, Any]], ToolResult]):
|
"""
|
||||||
|
表示一个可注册到 Agent 中的工具。
|
||||||
|
|
||||||
|
设计思路:
|
||||||
|
- 工具本身只关心自己的名字、说明和处理函数
|
||||||
|
- Agent 只依赖统一的 Tool 接口,而不关心具体工具细节
|
||||||
|
- 这样可以降低 Agent 与具体工具实现之间的耦合
|
||||||
|
|
||||||
|
参数:
|
||||||
|
name:
|
||||||
|
工具名称,必须唯一。
|
||||||
|
|
||||||
|
description:
|
||||||
|
工具用途说明。
|
||||||
|
主要用于教学展示、调试和未来做工具提示词时使用。
|
||||||
|
|
||||||
|
handler:
|
||||||
|
工具执行函数。
|
||||||
|
输入为字典,输出为 ToolResult。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
handler: Callable[[dict[str, Any]], ToolResult],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化一个工具对象。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
name:
|
||||||
|
工具名称。
|
||||||
|
|
||||||
|
description:
|
||||||
|
工具用途描述。
|
||||||
|
|
||||||
|
handler:
|
||||||
|
真正执行工具逻辑的函数。
|
||||||
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
|
|
||||||
def call(self, payload: dict[str, Any]) -> ToolResult:
|
def call(self, payload: dict[str, Any]) -> ToolResult:
|
||||||
|
"""
|
||||||
|
执行工具。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
payload:
|
||||||
|
传给工具的输入参数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
ToolResult:工具执行结果。
|
||||||
|
"""
|
||||||
return self.handler(payload)
|
return self.handler(payload)
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClient(Protocol):
|
||||||
|
"""
|
||||||
|
LLM 客户端协议。
|
||||||
|
|
||||||
|
这是一个非常重要的教学设计点:
|
||||||
|
|
||||||
|
我们不让 Agent 直接依赖某一个具体模型 SDK,
|
||||||
|
而是先定义一个统一接口。这样以后无论接:
|
||||||
|
|
||||||
|
- OpenAI
|
||||||
|
- Anthropic
|
||||||
|
- LiteLLM
|
||||||
|
- 本地模型
|
||||||
|
- 假模型(Fake LLM)
|
||||||
|
|
||||||
|
都可以复用同一个 Agent 主体。
|
||||||
|
|
||||||
|
方法:
|
||||||
|
stream_text:
|
||||||
|
输入当前消息列表,返回一个文本分块迭代器。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def stream_text(self, messages: list[Message]) -> Iterator[str]:
|
||||||
|
"""
|
||||||
|
基于当前消息列表,流式返回文本分块。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
messages:
|
||||||
|
当前上下文消息列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
一个字符串迭代器,每次 yield 一段文本。
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLLMClient:
|
||||||
|
"""
|
||||||
|
教学用假模型客户端。
|
||||||
|
|
||||||
|
这个类的核心价值是:
|
||||||
|
- 不依赖任何远程 API
|
||||||
|
- 方便本地测试
|
||||||
|
- 能模拟“流式输出”的基本行为
|
||||||
|
- 未来可以被真实 LLM 客户端直接替换
|
||||||
|
|
||||||
|
当前行为非常简单:
|
||||||
|
- 读取最后一条用户消息
|
||||||
|
- 拼成一段固定风格的回复
|
||||||
|
- 再把回复切成多个 chunk 流式返回
|
||||||
|
"""
|
||||||
|
|
||||||
|
def stream_text(self, messages: list[Message]) -> Iterator[str]:
|
||||||
|
"""
|
||||||
|
根据消息列表流式生成文本。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
messages:
|
||||||
|
当前消息列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
文本块迭代器。
|
||||||
|
"""
|
||||||
|
last_user_message = next(
|
||||||
|
(m.content for m in reversed(messages) if m.role == "user"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
response = f"[fake-llm] 你刚才说的是:{last_user_message}"
|
||||||
|
for chunk in self._chunk_text(response, size=8):
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _chunk_text(text: str, size: int = 8) -> Iterator[str]:
|
||||||
|
"""
|
||||||
|
将一段文本切成多个小块,用于模拟流式返回。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
text:
|
||||||
|
要切分的完整文本。
|
||||||
|
|
||||||
|
size:
|
||||||
|
每个文本块的最大长度。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
文本块迭代器。
|
||||||
|
"""
|
||||||
|
for i in range(0, len(text), size):
|
||||||
|
yield text[i : i + size]
|
||||||
|
|
||||||
|
|
||||||
class Agent:
|
class Agent:
|
||||||
def __init__(self) -> None:
|
"""
|
||||||
|
教学版 Agent 主类。
|
||||||
|
|
||||||
|
这是当前项目中最核心的对象。它负责:
|
||||||
|
- 保存消息
|
||||||
|
- 管理工具
|
||||||
|
- 管理简单记忆
|
||||||
|
- 调用 LLM
|
||||||
|
- 运行最小主循环
|
||||||
|
|
||||||
|
当前版本是教学最小骨架,因此刻意保持简单。
|
||||||
|
未来可以在这个类上继续演化:
|
||||||
|
- 权限系统
|
||||||
|
- system prompt
|
||||||
|
- tool calling 协议
|
||||||
|
- verification agent
|
||||||
|
- transcript 持久化
|
||||||
|
- context management
|
||||||
|
|
||||||
|
参数:
|
||||||
|
llm:
|
||||||
|
可注入的 LLM 客户端。
|
||||||
|
如果不传,则默认使用 FakeLLMClient。
|
||||||
|
|
||||||
|
属性:
|
||||||
|
messages:
|
||||||
|
当前 Agent 的消息历史。
|
||||||
|
|
||||||
|
tools:
|
||||||
|
已注册工具表,key 为工具名,value 为 Tool 实例。
|
||||||
|
|
||||||
|
memory:
|
||||||
|
教学版最小记忆列表。
|
||||||
|
当前只做演示用途。
|
||||||
|
|
||||||
|
max_turns:
|
||||||
|
最大回合数,避免无限循环。
|
||||||
|
|
||||||
|
llm:
|
||||||
|
当前绑定的 LLM 客户端实现。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, llm: LLMClient | None = None) -> None:
|
||||||
|
"""
|
||||||
|
初始化 Agent。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
llm:
|
||||||
|
可选的 LLM 客户端。
|
||||||
|
不传时使用默认的 FakeLLMClient。
|
||||||
|
"""
|
||||||
self.messages: list[Message] = []
|
self.messages: list[Message] = []
|
||||||
self.tools: dict[str, Tool] = {}
|
self.tools: dict[str, Tool] = {}
|
||||||
self.memory: list[str] = []
|
self.memory: list[str] = []
|
||||||
self.max_turns: int = 20
|
self.max_turns: int = 20
|
||||||
|
self.llm: LLMClient = llm or FakeLLMClient()
|
||||||
|
|
||||||
def register_tool(self, tool: Tool) -> None:
|
def register_tool(self, tool: Tool) -> None:
|
||||||
|
"""
|
||||||
|
注册一个工具到 Agent 中。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
tool:
|
||||||
|
要注册的工具对象。
|
||||||
|
"""
|
||||||
self.tools[tool.name] = tool
|
self.tools[tool.name] = tool
|
||||||
|
|
||||||
def add_message(self, role: str, content: str, **meta: Any) -> None:
|
def add_message(self, role: str, content: str, **meta: Any) -> None:
|
||||||
|
"""
|
||||||
|
向消息历史中追加一条消息。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
role:
|
||||||
|
消息角色。
|
||||||
|
|
||||||
|
content:
|
||||||
|
消息文本内容。
|
||||||
|
|
||||||
|
**meta:
|
||||||
|
任意附加元数据。
|
||||||
|
"""
|
||||||
self.messages.append(Message(role=role, content=content, meta=meta))
|
self.messages.append(Message(role=role, content=content, meta=meta))
|
||||||
|
|
||||||
def remember(self, text: str) -> None:
|
def remember(self, text: str) -> None:
|
||||||
|
"""
|
||||||
|
向简化记忆列表中加入一条记忆。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
text:
|
||||||
|
要保存的记忆文本。
|
||||||
|
"""
|
||||||
self.memory.append(text)
|
self.memory.append(text)
|
||||||
|
|
||||||
def can_use_tool(self, tool_name: str) -> bool:
|
def can_use_tool(self, tool_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
判断某个工具当前是否可用。
|
||||||
|
|
||||||
|
当前教学版逻辑非常简单:
|
||||||
|
- 只判断该工具是否已注册
|
||||||
|
|
||||||
|
未来可以扩展为:
|
||||||
|
- 权限判断
|
||||||
|
- allow / ask / deny
|
||||||
|
- agent 角色限制
|
||||||
|
|
||||||
|
参数:
|
||||||
|
tool_name:
|
||||||
|
工具名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
bool:是否可用。
|
||||||
|
"""
|
||||||
return tool_name in self.tools
|
return tool_name in self.tools
|
||||||
|
|
||||||
def load_skills(self, skills_dir: str | Path) -> list[str]:
|
def load_skills(self, skills_dir: str | Path) -> list[str]:
|
||||||
|
"""
|
||||||
|
从目录中发现可用 Skills。
|
||||||
|
|
||||||
|
当前教学版规则:
|
||||||
|
- 递归查找 `SKILL.md`
|
||||||
|
- skill 名称使用其父目录名
|
||||||
|
|
||||||
|
参数:
|
||||||
|
skills_dir:
|
||||||
|
Skills 根目录路径。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
已发现的 skill 名称列表。
|
||||||
|
"""
|
||||||
skills_path = Path(skills_dir)
|
skills_path = Path(skills_dir)
|
||||||
if not skills_path.exists():
|
if not skills_path.exists():
|
||||||
return []
|
return []
|
||||||
@@ -58,13 +363,62 @@ class Agent:
|
|||||||
loaded.append(path.parent.name)
|
loaded.append(path.parent.name)
|
||||||
return loaded
|
return loaded
|
||||||
|
|
||||||
|
def call_llm_stream(self) -> Iterator[str]:
|
||||||
|
"""
|
||||||
|
调用当前绑定的 LLM 客户端,并返回流式文本块。
|
||||||
|
|
||||||
|
这是一个关键的抽象层。
|
||||||
|
以后接真实模型时,优先改的是 LLMClient 的实现,
|
||||||
|
而不是 Agent 主循环本身。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
文本块迭代器。
|
||||||
|
"""
|
||||||
|
return self.llm.stream_text(self.messages)
|
||||||
|
|
||||||
def model_step(self) -> dict[str, Any]:
|
def model_step(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
执行一次模型步骤。
|
||||||
|
|
||||||
|
当前教学版逻辑:
|
||||||
|
- 调用 LLM 流式接口
|
||||||
|
- 收集所有 chunk
|
||||||
|
- 组装为一条 message 类型结果
|
||||||
|
|
||||||
|
在未来版本中,这里可以继续扩展为:
|
||||||
|
- message
|
||||||
|
- tool_call
|
||||||
|
- subagent_call
|
||||||
|
- verification_request
|
||||||
|
|
||||||
|
返回:
|
||||||
|
一个描述“本轮模型决定”的字典。
|
||||||
|
"""
|
||||||
|
chunks = list(self.call_llm_stream())
|
||||||
return {
|
return {
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"content": "这是一个教学用 Agent 骨架。下一步请在 model_step() 中接入你的模型逻辑。",
|
"content": "".join(chunks),
|
||||||
|
"chunks": chunks,
|
||||||
}
|
}
|
||||||
|
|
||||||
def run(self, user_input: str) -> str:
|
def run(self, user_input: str) -> str:
|
||||||
|
"""
|
||||||
|
运行 Agent 的最小主循环。
|
||||||
|
|
||||||
|
当前版本流程:
|
||||||
|
1. 记录用户输入
|
||||||
|
2. 调用 model_step()
|
||||||
|
3. 如果返回 message,则结束
|
||||||
|
4. 如果未来返回 tool_call,则执行工具并继续循环
|
||||||
|
5. 超过最大轮次则停止
|
||||||
|
|
||||||
|
参数:
|
||||||
|
user_input:
|
||||||
|
用户输入文本。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
Agent 最终返回给用户的文本。
|
||||||
|
"""
|
||||||
self.add_message("user", user_input)
|
self.add_message("user", user_input)
|
||||||
|
|
||||||
for turn in range(self.max_turns):
|
for turn in range(self.max_turns):
|
||||||
@@ -72,7 +426,12 @@ class Agent:
|
|||||||
|
|
||||||
if step["type"] == "message":
|
if step["type"] == "message":
|
||||||
content = step["content"]
|
content = step["content"]
|
||||||
self.add_message("assistant", content, turn=turn)
|
self.add_message(
|
||||||
|
"assistant",
|
||||||
|
content,
|
||||||
|
turn=turn,
|
||||||
|
chunks=step.get("chunks", []),
|
||||||
|
)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
if step["type"] == "tool_call":
|
if step["type"] == "tool_call":
|
||||||
@@ -81,7 +440,12 @@ class Agent:
|
|||||||
|
|
||||||
if not self.can_use_tool(tool_name):
|
if not self.can_use_tool(tool_name):
|
||||||
error = f"Tool not allowed or not found: {tool_name}"
|
error = f"Tool not allowed or not found: {tool_name}"
|
||||||
self.add_message("tool_result", error, ok=False, tool=tool_name)
|
self.add_message(
|
||||||
|
"tool_result",
|
||||||
|
error,
|
||||||
|
ok=False,
|
||||||
|
tool=tool_name,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result = self.tools[tool_name].call(tool_input)
|
result = self.tools[tool_name].call(tool_input)
|
||||||
@@ -103,5 +467,19 @@ class Agent:
|
|||||||
|
|
||||||
|
|
||||||
def echo_tool(payload: dict[str, Any]) -> ToolResult:
|
def echo_tool(payload: dict[str, Any]) -> ToolResult:
|
||||||
|
"""
|
||||||
|
一个最简单的教学示例工具。
|
||||||
|
|
||||||
|
作用:
|
||||||
|
把输入文本原样回显回来。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
payload:
|
||||||
|
工具输入字典。
|
||||||
|
约定使用 `text` 字段。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
ToolResult:工具执行结果。
|
||||||
|
"""
|
||||||
text = str(payload.get("text", ""))
|
text = str(payload.get("text", ""))
|
||||||
return ToolResult(ok=True, content=f"echo: {text}")
|
return ToolResult(ok=True, content=f"echo: {text}")
|
||||||
|
|||||||
Reference in New Issue
Block a user