03 · Agent 管线
架构剖析第三篇:Agent 生命周期、ConversationSession 工具循环、DialogueManager RAG 组合、Transferer 跨 Agent 派发、工具系统与客户端抽象。 对应
ARCHITECTURE.md§4。
§4.1 为什么不用现成 Agent 框架
| 框架 | 不选原因 |
|---|---|
| LangChain | 抽象层太厚,无法精细控制单 agent 生命周期、工具审批时机、记忆写入点 |
| AutoGen | 多 agent 协作模式与 Familiars 的三层架构(Manager → Assistant → Dialogue)不匹配 |
| CrewAI | 本地小模型(decider/rewriter/memorizer-writer)需要深度耦合主循环 |
自研管线把「规划 → 工具调用 → 记忆 → 报告」作一等公民,每个阶段都有明确的拦截点。
§4.2 Agent 类(src/models/assistant.py)
4.2.1 Assistant 构造
python
class Assistant:
def __init__(self, id: int, mode='normal', description: str = '',
name: str = '', notification_type: str = 'SYSTEM',
path_override: Optional[str] = None):
self.id = id
self.workspace_route = _resolve_workspace_root()
# 路径解析
if path_override:
# Program worker: 绝对路径,如 <repo>/runs/programs/<pid>/agents/<role>
self.path = path_override
_agent_dir = os.path.abspath(path_override)
_agents_parent = os.path.dirname(_agent_dir)
_memorizer_root = os.path.dirname(_agents_parent)
_agent_key = os.path.basename(_agent_dir)
self.memorizer = Memorizer(_memorizer_root, agent_id=_agent_key)
else:
self.path = f'{self.workspace_route}/workspace/agents/agent_{self.id}'
self.memorizer = Memorizer(
os.path.join(self.workspace_route, "workspace"),
agent_id=f"agent_{self.id}"
)
# 核心组件
self.vision_processor = VisionProcessor() # MultimodalTools
self.dialogue_manager = DialogueManager(
self.memorizer,
agent_path=self.path,
top_k=int(os.getenv("TOP_K") or 5),
workspace_route=self.workspace_route
)
# 通知类型自动检测
if self.notification_type == 'SYSTEM':
if self.info['os'] == 'Darwin':
self.notification_type = 'MacOS'
elif self.info['os'] == 'Windows':
self.notification_type = 'Windows'
else:
self.notification_type = 'DESKTOP'4.2.2 AssistantManager
python
class AssistantManager:
"""N 个 Assistant 的编排器。
职责:
- Agent 生命周期(创建/删除/列表)
- APScheduler cron 驱动报告聚合
- MANAGER_MODEL 独立模型用于执行摘要 + 人设演化
"""4.2.3 信号评估(Decider 集成)
python
def evaluate_signal(
self,
*,
trigger_type: str, # "new_message" / "external_info" / ...
trigger_summary: str, # 触发摘要
trigger_details: Optional[dict] = None,
memory: Optional[list[str]] = None,
user_state: Optional[dict] = None,
recent_user_messages: Optional[list[str]] = None,
):
"""Ask the decider whether this agent should act on a signal.
Returns a DecisionOutcome (fire: bool, action_category: str, act_score: float).
Always returns — never raises. Worst case: fire=False with source indicating why.
"""
from src.decider import DeciderService
svc = DeciderService.shared()
return svc.evaluate(
agent_id=self.id,
trigger_type=trigger_type,
trigger_summary=trigger_summary,
...
)4.2.4 主动消息
python
def send_proactive_message(
self,
*,
content: str,
signal_summary: str,
action_category: str,
act_score: Optional[float] = None,
decision_id: Optional[str] = None,
dialogue_id: int = 0,
title: Optional[str] = None,
fired_at: Optional[float] = None,
extra: Optional[dict] = None,
) -> Optional[int]:
"""Attach an agent-initiated A-node to dialogue_id and persist.
origin='proactive' + metadata → frontend rating UI can POST feedback.
Returns the new node id, or None if no dialogue exists.
"""4.2.5 通知通道
| 通道 | 实现 | 说明 |
|---|---|---|
| Windows | win10toast.ToastNotifier | 原生 toast;失败回退 desktop_notify |
| macOS | osascript -e 'display notification ...' | Notification Center |
| Desktop | src.services.desktop_notify.notify_desktop | Electron renderer → OS toast |
| Telegram | requests.post(api.telegram.org/bot{token}/sendMessage) | 远程推送 |
§4.3 ConversationSession(src/models/conversation.py)
4.3.1 构造参数
python
class ConversationSession:
def __init__(
self,
model: Optional[str] = None, # 默认 select_model(fallback_env="DEFAULT_MODEL")
timeout: Optional[int] = None, # 默认 TIMEOUT=300s
tools: Optional[list[dict]] = None, # OpenAI function schemas
system_prompt: str = "",
base_url: Optional[str] = None,
api_key: Optional[str] = None,
dispatcher: Optional[ToolDispatcher] = None,
):4.3.2 CircuitBreaker(熔断器)
python
class CircuitBreaker:
"""Per-URL 熔断器,三态:CLOSED → OPEN → HALF_OPEN。
CLOSED: 请求正常流动,计数失败。
OPEN: 所有请求立即拒绝 recovery_seconds。
HALF_OPEN: 允许一个探测请求;成功→CLOSED,失败→OPEN。
"""
def __init__(
self,
failure_threshold: int = 5, # CB_FAILURE_THRESHOLD
recovery_seconds: float = 30.0, # CB_RECOVERY_SECONDS
half_open_max: int = 1,
):全局共享:_breakers: dict[str, CircuitBreaker] 按 base URL 索引,所有 ConversationSession 实例共享同一个熔断器。
4.3.3 重试逻辑
python
_RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504}
# 永久性内容策略拒绝(不可重试)
_PERMANENT_CONTENT_ERROR_CODES = frozenset({
"local:sensitive_words", # api3.wlai.vip 中文内容过滤
"content_filter", # azure openai
"content_policy_violation", # openai
"safety", # gemini / vertex
"prohibited_content", # gemini
})4.3.4 流式决策
python
_REASONING_MODEL_PREFIXES = ("gpt-5", "gpt-o", "o1-", "o3-", "o4-")
_REASONING_MODEL_EXACT = frozenset({"o1", "o3", "o4"})
def _should_stream(model: str) -> bool:
"""推理模型(gpt-5.x, gpt-o1, gpt-o3)返回 False。
原因:推理模型的 SSE delta 协议与现有 assembler 不兼容 ——
HTTP 200 但 prompt_tokens=0, completion_tokens=0,延迟 100-200s。
环境变量覆盖:
FAMILIARS_STREAM_LLM=true → 强制开启(调试)
FAMILIARS_STREAM_LLM=false → 强制关闭
"""4.3.5 工具循环(contact() 方法)
python
async def contact(self, history: list[dict], **kwargs) -> dict:
"""Run the tool-call loop on top of chat().
每轮 LLM 可发 ≥1 工具调用 → dispatch → tool_result → 继续循环。
直到 max_turns 或 LLM 停止发工具调用。
Turn budget 升级:预算耗尽时询问 manager LLM 是否延长。
"""4.3.6 文档自动刷新
python
def _build_auto_refresh_messages(path_norm: str, write_tool: str,
progress_footer: str = "") -> list[dict]:
"""在 write 操作后合成 (assistant tool_call, tool result) 对。
下次 mutation 同一文件时,_mark_stale_docx_reads 会折叠旧内容。
返回两条消息准备追加到 history。
"""§4.4 DialogueManager
python
class DialogueManager:
"""管理 N 个 Dialogue(对话树 + 历史)。
职责:
- _load_existing_dialogues() 从 disk 读取
- store_dialogue(dialogue_id) 持久化
- build_rag_context(query) 组装 RAG 上下文
- release_memory() 释放常驻内存(JIT 按需装载)
"""
def __init__(self, memorizer, agent_path, top_k=5, workspace_route=""):
self.memorizer = memorizer
self.agent_path = agent_path
self.top_k = top_k
self.dialogue: list[Dialogue] = []§4.5 Transferer(跨 Agent 派发)
src/models/transferer.py 是所有跨 agent 调用的唯一咽喉点。
python
def dispose(request_model: str, request: str, **kwargs) -> Any:
"""将 destination 字符串解析为可调用对象。
优先级:
1. 工具客户端别名 — "BrowseTool_Client", "browser_tools" → 实例化 ToolClient
2. 工具方法路径 — "BaseTools.open_app" → 解析到 src.utils.* 的方法
3. 模型名 — 直接发 chat completion(无工具 schema)
4. 空 / "Writer" / 未知 → 回退到 DEFAULT_MODEL
"""客户端注册表
python
_CLIENT_REGISTRY: dict[str, str] = {
"basetoolclient": "src.models.clients.base_client:BaseToolClient",
"browsertoolclient": "src.models.clients.browser_client:BrowserToolClient",
"webdive": "src.models.clients.browser_client:WebDiveToolClient",
"multimodaltoolclient": "src.models.clients.multimodal_client:MultimodalToolClient",
"inputtoolclient": "src.models.clients.input_client:InputToolClient",
"doctoolclient": "src.models.clients.doc_client:DocToolClient",
"dailytoolclient": "src.models.clients.daily_client:DailyToolClient",
"systemtoolclient": "src.models.clients.system_client:SystemToolClient",
# ...
}§4.6 工具系统架构
4.6.1 ToolDispatcher
python
class ToolDispatcher:
"""工具名 → 可调用的注册表。
15 个默认工具实例:
"""
_DEFAULT_INSTANCES = [
("BaseTools", BaseTools),
("BaseTools.InputController", BaseTools.InputController),
("BrowserTools", BrowserTools),
("MultimodalTools", MultimodalTools),
("DailyTools", DailyTools),
("DailyMonitorTools", DailyMonitorTools),
("DocTools", DocTools),
("MemoryTools", MemoryTools),
("SystemTools", SystemTools),
("UserTools", UserTools),
("UserSecretTools", UserSecretTools),
("PhoneTools", PhoneTools),
("SkillTools", SkillTools),
("SkillWatcherTools", SkillWatcherTools),
("BrokerTools", BrokerTools),
]4.6.2 execute_skill 伪工具
python
def execute_skill_schema() -> dict:
"""OpenAI function schema for execute_skill pseudo-tool.
框架级 affordance,注册在每个 ToolDispatcher 上。
Recipe/flow unit 通过 tools.whitelist 暴露此 schema。
"""
return {
"type": "function",
"function": {
"name": "execute_skill",
"parameters": {
"type": "object",
"properties": {
"skill_id": {"type": "string"},
"args": {"type": "object"},
},
"required": ["skill_id"],
},
},
}4.6.3 工具活动映射
python
_INSTANCE_ACTIVITY_KEY: dict[str, str] = {
"BaseTools": "base",
"BrowserTools": "browser",
"MultimodalTools": "multimodal",
"DailyTools": "daily",
"DailyMonitorTools": "daily",
"DocTools": "doc",
"SystemTools": "system",
"InputController": "input",
}驱动前端 robot-mascot 状态切换。
4.6.4 客户端工具白名单
每个 ToolClient 子类只暴露它能用的工具:
| 客户端 | 工具子集 |
|---|---|
BaseToolClient | 基础工具(read/write/execute/find/grep/download) |
BrowserToolClient | 浏览器工具 + explore_page |
MultimodalToolClient | 视觉 + 操作工具 |
DocToolClient | 文档工具(Word/Excel/PPT/PDF) |
DailyToolClient | 日程/待办/提醒 |
SystemToolClient | 系统工具 |
Schema 来自 workspace/tools.json + filtered_schemas() 按客户端过滤。
§4.7 Turn Budget 升级器
python
# max_turns 不再是硬顶
# 预算耗尽时询问 manager LLM:
# - 延长(+N turns)
# - 中止
# 实现:_turn_budget_escalator 调用 MANAGER_MODEL 判断下一篇:04-RAG 与记忆 →
