feat: enhance agent orchestration, knowledge flow and UI refinements

This commit is contained in:
2026-03-29 20:31:13 +08:00
parent d85cb9cf35
commit e0fe3ca623
301 changed files with 1197804 additions and 7863 deletions

View File

@@ -1,397 +1,354 @@
"""
Jarvis LangGraph Agent 主图定义
Jarvis LangGraph Agent 主图定义 - 优化重构版
"""
import json
import logging
import re
from typing import Literal, Union, List, Any
from langchain_core.messages import (
BaseMessage,
HumanMessage,
AIMessage,
SystemMessage,
ToolMessage
)
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from app.agents.state import AgentState, AgentRole
from app.agents.prompts import (
MASTER_SYSTEM_PROMPT,
PLANNER_SYSTEM_PROMPT,
SCHEDULE_PLANNER_SYSTEM_PROMPT,
EXECUTOR_SYSTEM_PROMPT,
LIBRARIAN_SYSTEM_PROMPT,
ANALYST_SYSTEM_PROMPT,
PLANNER_SCOPE_PROMPT,
PLANNER_STEPS_PROMPT,
EXECUTOR_TASKS_PROMPT,
EXECUTOR_FORUM_PROMPT,
LIBRARIAN_RETRIEVAL_PROMPT,
LIBRARIAN_GRAPH_PROMPT,
ANALYST_PROGRESS_PROMPT,
ANALYST_INSIGHTS_PROMPT,
JSON_ACTION_FALLBACK_PROMPT,
)
from app.agents.tools import ALL_TOOLS, SUB_COMMANDER_TOOLSETS
from app.agents.tools.time_reasoning import normalize_tool_time_arguments
from app.agents.skill_registry import build_skill_context
from app.services.llm_service import get_llm
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_ollama import ChatOllama
import httpx
from app.services.llm_service import (
get_llm,
create_llm_from_config,
resolve_provider_capabilities,
default_provider_capabilities
)
from app.logging_utils import summarize_llm_config
logger = logging.getLogger("jarvis.agent")
SUB_COMMANDER_PROMPTS = {
"planner_scope": PLANNER_SCOPE_PROMPT,
"planner_steps": PLANNER_STEPS_PROMPT,
"executor_tasks": EXECUTOR_TASKS_PROMPT,
"executor_forum": EXECUTOR_FORUM_PROMPT,
"librarian_retrieval": LIBRARIAN_RETRIEVAL_PROMPT,
"librarian_graph": LIBRARIAN_GRAPH_PROMPT,
"analyst_progress": ANALYST_PROGRESS_PROMPT,
"analyst_insights": ANALYST_INSIGHTS_PROMPT,
}
ROLE_SUB_COMMANDERS = {
AgentRole.PLANNER: ["planner_scope", "planner_steps"],
AgentRole.EXECUTOR: ["executor_tasks", "executor_forum"],
AgentRole.LIBRARIAN: ["librarian_retrieval", "librarian_graph"],
AgentRole.ANALYST: ["analyst_progress", "analyst_insights"],
}
ROLE_SKILL_CONTEXT = {
AgentRole.PLANNER: "planner",
AgentRole.EXECUTOR: "executor",
AgentRole.LIBRARIAN: "librarian",
AgentRole.ANALYST: "analyst",
}
def _create_llm_from_config(config: dict):
"""根据用户模型配置创建 LLM 实例"""
provider = config.get("provider", "openai")
model = config.get("model", "")
api_key = config.get("api_key", "")
base_url = config.get("base_url", "")
if provider == "openai" or provider == "deepseek" or provider == "custom":
return ChatOpenAI(
api_key=api_key,
model=model,
base_url=base_url or None,
timeout=httpx.Timeout(60.0, connect=10.0),
)
elif provider == "claude":
return ChatAnthropic(
api_key=api_key,
model=model,
timeout=httpx.Timeout(60.0, connect=10.0),
)
elif provider == "ollama":
return ChatOllama(
base_url=base_url or "http://localhost:11434",
model=model,
timeout=httpx.Timeout(120.0, connect=10.0),
)
else:
return ChatOpenAI(
api_key=api_key,
model=model,
base_url=base_url or None,
timeout=httpx.Timeout(60.0, connect=10.0),
)
# ===================== 工具辅助函数 =====================
def _get_llm_for_state(state: AgentState):
"""从 state 获取 LLM 实例,优先使用用户配置的模型"""
"""获取配置好的 LLM 实例"""
user_llm_config = state.get("user_llm_config")
if user_llm_config:
return _create_llm_from_config(user_llm_config)
return get_llm()
llm = create_llm_from_config(user_llm_config) if user_llm_config else get_llm()
# 注入解析到的能力
capabilities = getattr(llm, "_jarvis_provider_capabilities", None)
if capabilities is None:
capabilities = resolve_provider_capabilities(user_llm_config) if user_llm_config else default_provider_capabilities()
state["provider_capabilities"] = {
"provider": capabilities.provider,
"supports_native_tools": capabilities.supports_native_tools,
"preferred_tool_strategy": capabilities.preferred_tool_strategy,
}
return llm, capabilities
async def _ainvoke(llm, messages: list[BaseMessage]):
ainvoke = getattr(llm, "ainvoke", None)
if callable(ainvoke):
return await ainvoke(messages)
return await llm.invoke(messages)
def _filter_user_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
return [m for m in messages if m.type in ("human", "user")]
async def _ainvoke_with_tools(llm, messages: list[BaseMessage], tools=None):
toolset = tools if tools is not None else ALL_TOOLS
bound_llm = llm.bind_tools(toolset)
if hasattr(bound_llm, "ainvoke"):
return await bound_llm.ainvoke(messages)
return await bound_llm.invoke(messages)
def _compile_graph(graph: StateGraph, callbacks: list | None = None):
if callbacks:
try:
return graph.compile(callbacks=callbacks)
except TypeError as exc:
if "callbacks" not in str(exc):
raise
return graph.compile()
def _msg_type(msg: BaseMessage) -> str:
"""Get message type, handles both .type (new) and .role (old) attribute names."""
return getattr(msg, "type", None) or getattr(msg, "role", "human")
def _filter_user_messages(messages: list) -> list[BaseMessage]:
return [m for m in messages if _msg_type(m) in ("human", "user")]
def _normalize_user_text(text: str) -> str:
return (text or "").strip().lower()
def _is_simple_greeting(text: str) -> bool:
normalized = _normalize_user_text(text)
return normalized in {"你好", "您好", "", "早上好", "在吗", "", "hi", "hello"}
def _is_identity_question(text: str) -> bool:
normalized = _normalize_user_text(text)
return normalized in {"你是谁", "你是誰"}
def _is_capability_question(text: str) -> bool:
normalized = _normalize_user_text(text)
return normalized in {"你能做什么", "你可以做什么", "你会做什么"}
def _choose_sub_commander(role: AgentRole, user_query: str) -> str:
text = _normalize_user_text(user_query)
if role == AgentRole.PLANNER:
if any(keyword in text for keyword in ["步骤", "计划", "拆解", "排期", "优先级", "路线"]):
return "planner_steps"
return "planner_scope"
def _get_role_tools(role: AgentRole) -> list:
"""获取角色对应的所有可用工具集"""
if role == AgentRole.SCHEDULE_PLANNER:
# 合并分析和规划工具
return list(set(SUB_COMMANDER_TOOLSETS["schedule_analysis"] + SUB_COMMANDER_TOOLSETS["schedule_planning"]))
if role == AgentRole.EXECUTOR:
if any(keyword in text for keyword in ["论坛", "帖子", "发帖", "指令", "discussion", "instruction"]):
return "executor_forum"
return "executor_tasks"
return list(set(SUB_COMMANDER_TOOLSETS["executor_tasks"] + SUB_COMMANDER_TOOLSETS["executor_forum"]))
if role == AgentRole.LIBRARIAN:
if any(keyword in text for keyword in ["图谱", "关系", "构建", "沉淀", "节点", "graph"]):
return "librarian_graph"
return "librarian_retrieval"
return list(set(SUB_COMMANDER_TOOLSETS["librarian_retrieval"] + SUB_COMMANDER_TOOLSETS["librarian_graph"]))
if role == AgentRole.ANALYST:
if any(keyword in text for keyword in ["趋势", "风险", "洞察", "建议", "机会", "insight"]):
return "analyst_insights"
return "analyst_progress"
return ROLE_SUB_COMMANDERS[role][0]
return list(set(SUB_COMMANDER_TOOLSETS["analyst_progress"] + SUB_COMMANDER_TOOLSETS["analyst_insights"]))
return []
def _record_sub_commander(state: AgentState, sub_commander: str, user_query: str):
state["current_sub_commander"] = sub_commander
state["active_sub_commanders"] = state.get("active_sub_commanders", []) + [sub_commander]
state["sub_commander_trace"] = state.get("sub_commander_trace", []) + [{
"agent": state.get("current_agent", AgentRole.MASTER).value,
"sub_commander": sub_commander,
"query": user_query,
}]
# ===================== 核心执行逻辑 (ReAct) =====================
def _build_system_messages(state: AgentState, system_prompt: str, role: AgentRole):
system_msgs: list[BaseMessage] = [SystemMessage(content=system_prompt)]
skill_ctx = build_skill_context(ROLE_SKILL_CONTEXT[role])
async def call_agent_llm(state: AgentState, role: AgentRole, system_prompt: str) -> dict:
"""通用的 LLM 调用节点逻辑"""
llm, capabilities = _get_llm_for_state(state)
tools = _get_role_tools(role)
# 构建消息序列
messages = []
# 1. 系统提示词
messages.append(SystemMessage(content=system_prompt))
# 2. 环境上下文 (时间、记忆等)
if state.get("current_datetime_context"):
messages.append(SystemMessage(content=f"当前时间上下文: {state['current_datetime_context']}"))
if state.get("memory_context"):
messages.append(SystemMessage(content=f"长期记忆上下文: {state['memory_context']}"))
# 3. 技能增强
role_skill_key = role.value.replace("agent_", "")
skill_ctx = build_skill_context(role_skill_key)
if skill_ctx:
system_msgs.append(SystemMessage(content=skill_ctx))
return system_msgs
messages.append(SystemMessage(content=skill_ctx))
# 4. 历史对话 (add_messages 已经处理好了)
messages.extend(state["messages"])
# 绑定工具
if tools and capabilities.supports_native_tools:
llm_with_tools = llm.bind_tools(tools)
else:
llm_with_tools = llm
if tools: # 如果有工具但不支持原生,注入 JSON Fallback 提示
messages.append(SystemMessage(content=JSON_ACTION_FALLBACK_PROMPT))
tool_names = [t.name for t in tools]
messages.append(SystemMessage(content=f"本次可用工具列表: {', '.join(tool_names)}"))
logger.info(
f"agent_node_started",
extra={
"details": {
"role": role.value,
"message_count": len(messages),
"tool_count": len(tools),
"provider": capabilities.provider
}
}
)
# 执行调用
response = await llm_with_tools.ainvoke(messages)
logger.info(
f"agent_node_finished",
extra={
"details": {
"role": role.value,
"has_tool_calls": bool(getattr(response, "tool_calls", None)),
"content_length": len(response.content) if response.content else 0
}
}
)
return {"messages": [response]}
async def _run_sub_commander(
state: AgentState,
role: AgentRole,
manager_prompt: str,
user_query: str,
*,
use_tools: bool,
summary_target: str | None = None,
):
llm = _get_llm_for_state(state)
sub_commander = _choose_sub_commander(role, user_query)
_record_sub_commander(state, sub_commander, user_query)
toolset = SUB_COMMANDER_TOOLSETS.get(sub_commander, [])
system_msgs = _build_system_messages(state, manager_prompt, role)
system_msgs.append(SystemMessage(content=f"本次应由子指挥官 `{sub_commander}` 接手。请严格按该角色职责输出。"))
system_msgs.append(SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander]))
if use_tools and toolset:
response = await _ainvoke_with_tools(
llm,
system_msgs + [HumanMessage(content=f"用户请求: {user_query}")],
toolset,
async def execute_tools_node(state: AgentState) -> dict:
"""执行工具调用并返回 ToolMessage 的通用节点"""
last_message = state["messages"][-1]
if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
return {"messages": []}
tool_map = {t.name: t for t in ALL_TOOLS}
tool_messages = []
created_entities = []
for tool_call in last_message.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
tool_id = tool_call.get("id")
logger.info(
f"tool_execution_started",
extra={
"details": {
"tool_name": tool_name,
"tool_args": tool_args,
"tool_id": tool_id
}
}
)
tool_calls = getattr(response, "tool_calls", None) or []
if tool_calls:
results = []
for tc in tool_calls:
tool_name = tc.get("name")
args = tc.get("args", {})
for tool in toolset:
if tool.name == tool_name:
try:
result = tool.invoke(args)
results.append(f"[{tool_name}] {result}")
except Exception as e:
results.append(f"[{tool_name}] 执行失败: {e}")
break
state["tool_calls"] = tool_calls
state["last_tool_result"] = "\n".join(results)
follow_up = await _ainvoke(
llm,
[
SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander]),
HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}")
]
try:
# 时间参数归一化
normalized_args = normalize_tool_time_arguments(
tool_name,
tool_args,
state.get("current_datetime_context")
)
state["final_response"] = follow_up.content
else:
state["final_response"] = response.content
else:
response = await _ainvoke(
llm,
system_msgs + [HumanMessage(content=f"用户请求: {user_query}")],
)
state["final_response"] = response.content
if summary_target:
state[summary_target] = state.get("final_response", "")
state["should_respond"] = True
return state
# ===================== 节点定义 (async) =====================
async def master_node(state: AgentState) -> AgentState:
"""主Agent节点: 理解用户意图决定调用哪个子Agent"""
messages: list[BaseMessage] = state["messages"]
user_msgs = _filter_user_messages(messages)
user_query = user_msgs[-1].content.strip() if user_msgs else ""
if _is_simple_greeting(user_query):
state["final_response"] = "您好。我在。\n\n您把问题给我,我先帮您收束重点,再往下推。"
state["should_respond"] = True
return state
if _is_identity_question(user_query):
state["final_response"] = "我是 Jarvis。\n\n比起做一个泛泛的助手,我更像您的判断型协作伙伴:帮您看清问题、压缩路径、把事情往前推进。"
state["should_respond"] = True
return state
if _is_capability_question(user_query):
state["final_response"] = "主要做三件事。\n- 帮您判断:看问题本质、梳理取舍、给出方向\n- 帮您收束:把复杂内容理顺,把重点拎出来\n- 帮您推进:拆任务、定步骤、把下一步变清楚\n\n如果您现在有具体目标,我可以直接进入处理。"
state["should_respond"] = True
return state
llm = _get_llm_for_state(state)
system_msgs: list[BaseMessage] = [SystemMessage(content=MASTER_SYSTEM_PROMPT)]
memory_ctx = state.get("memory_context")
if memory_ctx:
system_msgs.append(
SystemMessage(content=f"\n\n【记忆上下文】\n{memory_ctx}\n\n---\n")
tool = tool_map.get(tool_name)
if not tool:
result = f"Error: Tool {tool_name} not found."
else:
result = await tool.ainvoke(normalized_args) if hasattr(tool, "ainvoke") else tool.invoke(normalized_args)
# 实体识别(用于业务追踪)
if any(k in tool_name for k in ["create", "add", "new"]):
created_entities.append({"tool": tool_name, "result": str(result)})
status = "success"
except Exception as e:
logger.exception(f"tool_execution_failed: {tool_name}")
result = f"Error executing tool {tool_name}: {str(e)}"
status = "failed"
tool_messages.append(ToolMessage(
tool_call_id=tool_id,
content=str(result),
name=tool_name
))
logger.info(
f"tool_execution_finished",
extra={
"details": {
"tool_name": tool_name,
"status": status,
"result_preview": str(result)[:200]
}
}
)
response: AIMessage = await _ainvoke(llm, system_msgs + messages)
return {
"messages": tool_messages,
"created_entities": state.get("created_entities", []) + created_entities
}
# ===================== 各角色节点定义 =====================
async def master_node(state: AgentState) -> dict:
"""主控节点:负责意图识别与初步分发"""
user_messages = _filter_user_messages(state["messages"])
if not user_messages:
return {"final_response": "未收到有效输入。"}
query = user_messages[-1].content.strip()
# 快捷回复逻辑 (保留原有的人性化设计)
if re.match(r"^(你好|早|在吗|嗨|hi|hello)", query.lower()):
return {"final_response": "您好。我在。\n\n您把问题给我,我先帮您收束重点,再往下推。", "messages": [AIMessage(content="您好。我在。")]}
llm, capabilities = _get_llm_for_state(state)
# 路由判断:让 LLM 决定跳转到哪个角色,或者直接回答
# 这里我们使用一个简洁的提示词让 LLM 输出角色名称或直接回答
system_msg = SystemMessage(content=MASTER_SYSTEM_PROMPT + "\n\n请直接输出接下来该由哪个 Agent 接手(role_name),如果直接回答,请正常输出。")
response = await llm.ainvoke([system_msg] + state["messages"])
content = response.content.strip().lower()
if any(kw in content for kw in ["搜索", "查找", "知识", "检索"]):
next_agent = AgentRole.LIBRARIAN
elif any(kw in content for kw in ["计划", "安排", "拆解", "规划"]):
next_agent = AgentRole.PLANNER
elif any(kw in content for kw in ["执行", "", "操作", "创建", "更新"]):
next_agent = AgentRole.EXECUTOR
elif any(kw in content for kw in ["分析", "报告", "统计", "总结"]):
next_agent = AgentRole.ANALYST
else:
state["final_response"] = response.content
state["should_respond"] = True
return state
state["current_agent"] = next_agent
state["current_sub_commander"] = None
state["active_agents"] = state.get("active_agents", [AgentRole.MASTER]) + [next_agent]
state["should_respond"] = True
return state
# 简单的角色映射识别
roles = {r.value: r for r in AgentRole}
target_role = None
for r_val, r_enum in roles.items():
if r_val in content and len(content) < 50: # 如果内容很短且包含角色名,视为路由
target_role = r_enum
break
if target_role and target_role != AgentRole.MASTER:
logger.info(f"master_routing_decided: {target_role.value}")
return {
"current_agent": target_role.value,
"agent_trace": state.get("agent_trace", []) + [target_role.value],
"messages": [AIMessage(content=f"已分发至 {target_role.value} 处理。")]
}
return {"final_response": response.content, "messages": [response]}
async def planner_node(state: AgentState) -> AgentState:
"""规划Agent节点: 制定计划,拆解任务步骤"""
user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else ""
return await _run_sub_commander(state, AgentRole.PLANNER, PLANNER_SYSTEM_PROMPT, user_query, use_tools=False)
async def planner_node(state: AgentState) -> dict:
return await call_agent_llm(state, AgentRole.SCHEDULE_PLANNER, SCHEDULE_PLANNER_SYSTEM_PROMPT)
async def executor_node(state: AgentState) -> dict:
return await call_agent_llm(state, AgentRole.EXECUTOR, EXECUTOR_SYSTEM_PROMPT)
async def librarian_node(state: AgentState) -> dict:
return await call_agent_llm(state, AgentRole.LIBRARIAN, LIBRARIAN_SYSTEM_PROMPT)
async def analyst_node(state: AgentState) -> dict:
return await call_agent_llm(state, AgentRole.ANALYST, ANALYST_SYSTEM_PROMPT)
async def executor_node(state: AgentState) -> AgentState:
"""执行Agent节点: 调用工具执行具体任务"""
user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else ""
return await _run_sub_commander(state, AgentRole.EXECUTOR, EXECUTOR_SYSTEM_PROMPT, user_query, use_tools=True)
# ===================== 路由逻辑 =====================
def route_after_agent(state: AgentState) -> Literal["tools", "__end__"]:
"""判断 Agent 执行后是该走工具节点还是结束"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return END
async def librarian_node(state: AgentState) -> AgentState:
"""知识管理员节点: 管理知识库和知识图谱"""
user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else ""
return await _run_sub_commander(state, AgentRole.LIBRARIAN, LIBRARIAN_SYSTEM_PROMPT, user_query, use_tools=True, summary_target="knowledge_context")
async def analyst_node(state: AgentState) -> AgentState:
"""分析师节点: 分析工作数据,生成报告"""
user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else ""
return await _run_sub_commander(state, AgentRole.ANALYST, ANALYST_SYSTEM_PROMPT, user_query, use_tools=True, summary_target="analysis_report")
def route_agent(state: AgentState) -> str:
"""路由函数: 决定下一个节点"""
def route_master(state: AgentState) -> str:
"""主控路由逻辑"""
if state.get("final_response"):
return END
return state.get("current_agent", AgentRole.MASTER).value
return state.get("current_agent", END)
# ===================== 构建 =====================
# ===================== 构建 =====================
def create_agent_graph(callbacks: list | None = None):
graph = StateGraph(AgentState)
workflow = StateGraph(AgentState)
graph.add_node(AgentRole.MASTER.value, master_node)
graph.add_node(AgentRole.PLANNER.value, planner_node)
graph.add_node(AgentRole.EXECUTOR.value, executor_node)
graph.add_node(AgentRole.LIBRARIAN.value, librarian_node)
graph.add_node(AgentRole.ANALYST.value, analyst_node)
# 添加节点
workflow.add_node(AgentRole.MASTER.value, master_node)
workflow.add_node(AgentRole.SCHEDULE_PLANNER.value, planner_node)
workflow.add_node(AgentRole.EXECUTOR.value, executor_node)
workflow.add_node(AgentRole.LIBRARIAN.value, librarian_node)
workflow.add_node(AgentRole.ANALYST.value, analyst_node)
workflow.add_node("tools", execute_tools_node)
graph.set_entry_point(AgentRole.MASTER.value)
# 设置入口
workflow.set_entry_point(AgentRole.MASTER.value)
graph.add_conditional_edges(
# 主控分发逻辑
workflow.add_conditional_edges(
AgentRole.MASTER.value,
route_agent,
route_master,
{
AgentRole.PLANNER.value: AgentRole.PLANNER.value,
AgentRole.SCHEDULE_PLANNER.value: AgentRole.SCHEDULE_PLANNER.value,
AgentRole.EXECUTOR.value: AgentRole.EXECUTOR.value,
AgentRole.LIBRARIAN.value: AgentRole.LIBRARIAN.value,
AgentRole.ANALYST.value: AgentRole.ANALYST.value,
END: END,
END: END
}
)
for role in [AgentRole.PLANNER, AgentRole.EXECUTOR, AgentRole.LIBRARIAN, AgentRole.ANALYST]:
graph.add_edge(role.value, END)
# 各角色节点的 ReAct 循环
for role in [AgentRole.SCHEDULE_PLANNER, AgentRole.EXECUTOR, AgentRole.LIBRARIAN, AgentRole.ANALYST]:
workflow.add_conditional_edges(
role.value,
route_after_agent,
{
"tools": "tools",
END: END
}
)
# 工具执行完后回到当前 Agent 角色继续处理
workflow.add_conditional_edges(
"tools",
lambda s: s.get("current_agent", AgentRole.MASTER.value),
{
AgentRole.SCHEDULE_PLANNER.value: AgentRole.SCHEDULE_PLANNER.value,
AgentRole.EXECUTOR.value: AgentRole.EXECUTOR.value,
AgentRole.LIBRARIAN.value: AgentRole.LIBRARIAN.value,
AgentRole.ANALYST.value: AgentRole.ANALYST.value,
}
)
return _compile_graph(graph, callbacks=callbacks)
# 编译
if callbacks:
return workflow.compile(callbacks=callbacks)
return workflow.compile()
_agent_graph = None
def get_agent_graph(callbacks: list | None = None):
"""
获取编译好的 Agent 图(单例缓存)。
Callbacks 在首次编译时固定注入,后续调用忽略 callbacks 参数。
如需变更 Callbacks如修改 LANGCHAIN_PROJECT需重启服务。
Args:
callbacks: 可选的额外 Callbacks会与全局 LangSmith Callbacks 合并
"""
global _agent_graph
if _agent_graph is None:
from app.config_tracing import get_langsmith_callbacks