提供function call文件注释

master
KINGMAKER\67483 4 days ago
parent f521ea29c9
commit 09318326e2

@ -1,4 +1,13 @@
"""LangGraph ReAct heart: START → llm_call ⇄ tool_node → END. No global llm/agent.""" """LangGraph ReAct 核心START → llm_call ⇄ tool_node → END。
Function Call 在这里怎么走
1. 模型不会自己执行 Python只会在回复里写出 tool_calls函数名 + 参数
2. _should_continue 看到 tool_calls就把控制权交给 tool_node
3. tool_node 按名字找到真正的函数并执行结果写成 ToolMessage 交回模型
4. 再进 llm_call模型看观察结果决定继续调工具还是直接回答
这就是 ReActReason Act调工具 Observe看结果 Reason
"""
from __future__ import annotations from __future__ import annotations
@ -16,12 +25,20 @@ from gcode.models.openai_compatible import extract_reasoning, extract_text
from gcode.prompt import dynamic_context, static_system_prompt from gcode.prompt import dynamic_context, static_system_prompt
from gcode.session import Session from gcode.session import Session
# 同一轮对话最多问模型这么多次,防止「调工具 → 再想 → 再调」死循环烧钱。
# 40 不是官方标准,也不是 LangGraph / OpenAI 的规定,只是本项目初始化时拍的经验上限:
# - 日常改文件大概 515 次就够(读 → 改 → 跑 → 修)
# - 再大的任务应拆成下一轮,而不是一轮里无限转
# 容易混淆的官方数字LangGraph 的 recursion_limit默认曾是 251.0.6+ 改为 1000
# 数的是图节点步数llm_call 和 tool_node 各算一步),不是「问模型几次」。
# 本常量只拦 llm_call图本身没改 recursion_limit复杂一轮仍可能先撞框架上限。
MAX_LLM_CALLS = 40 MAX_LLM_CALLS = 40
class MessagesState(TypedDict): class MessagesState(TypedDict):
# operator.add节点返回的新消息会「追加」到列表而不是覆盖整段历史。
messages: Annotated[list[AnyMessage], operator.add] messages: Annotated[list[AnyMessage], operator.add]
llm_calls: int llm_calls: int # 本轮已经调用模型的次数
class Agent: class Agent:
@ -35,7 +52,9 @@ class Agent:
has_browser: bool = False, has_browser: bool = False,
cwd: Path | None = None, cwd: Path | None = None,
) -> None: ) -> None:
# 运行时按名字找函数:模型只输出 "read_file",这里才能对上真正的 Python 工具。
self._tools = {t.name: t for t in tools} self._tools = {t.name: t for t in tools}
# 把工具的 schema名字、参数、说明告诉模型它才会在回复里生成 tool_calls。
self._model = model.bind_tools(tools) self._model = model.bind_tools(tools)
self._session = session self._session = session
self._model_id = model_id self._model_id = model_id
@ -46,24 +65,28 @@ class Agent:
self._graph = self._build(checkpointer) self._graph = self._build(checkpointer)
def _build(self, checkpointer: Any) -> Any: def _build(self, checkpointer: Any) -> Any:
# 搭一张状态图:两个节点,一条条件边决定「调工具」还是「结束」。
builder = StateGraph(MessagesState) builder = StateGraph(MessagesState)
builder.add_node("llm_call", self._llm_call) builder.add_node("llm_call", self._llm_call)
builder.add_node("tool_node", self._tool_node) builder.add_node("tool_node", self._tool_node)
builder.add_edge(START, "llm_call") builder.add_edge(START, "llm_call")
builder.add_conditional_edges( builder.add_conditional_edges(
"llm_call", "llm_call",
self._should_continue, self._should_continue, # 看最后一条消息有没有 tool_calls
{"tool_node": "tool_node", END: END}, {"tool_node": "tool_node", END: END},
) )
# 工具跑完必须再回模型,否则观察结果没人读。
builder.add_edge("tool_node", "llm_call") builder.add_edge("tool_node", "llm_call")
return builder.compile(checkpointer=checkpointer) return builder.compile(checkpointer=checkpointer)
def _graph_config(self) -> dict[str, Any] | None: def _graph_config(self) -> dict[str, Any] | None:
# checkpointer 需要 thread_id 才能把同一会话的消息存下来、下次接着聊。
if self._session and self._session.checkpointer: if self._session and self._session.checkpointer:
return self._session.config() return self._session.config()
return None return None
async def invoke_turn(self, user_text: str) -> list[AnyMessage]: async def invoke_turn(self, user_text: str) -> list[AnyMessage]:
"""整轮跑完再返回全部消息(测试 / 非流式用)。"""
payload: MessagesState = { payload: MessagesState = {
"messages": [HumanMessage(content=user_text)], "messages": [HumanMessage(content=user_text)],
"llm_calls": 0, "llm_calls": 0,
@ -73,6 +96,7 @@ class Agent:
return list(result["messages"]) return list(result["messages"])
async def stream_turn(self, user_text: str) -> AsyncIterator[AgentEvent]: async def stream_turn(self, user_text: str) -> AsyncIterator[AgentEvent]:
"""边跑边吐事件:思考文字、工具开始/结束,给 TUI 用。"""
payload: MessagesState = { payload: MessagesState = {
"messages": [HumanMessage(content=user_text)], "messages": [HumanMessage(content=user_text)],
"llm_calls": 0, "llm_calls": 0,
@ -88,6 +112,7 @@ class Agent:
yield AgentEvent(type="error", text=f"{type(exc).__name__}: {exc}") yield AgentEvent(type="error", text=f"{type(exc).__name__}: {exc}")
async def _llm_call(self, state: MessagesState) -> dict[str, Any]: async def _llm_call(self, state: MessagesState) -> dict[str, Any]:
"""问模型一次。返回值只含「本节点新增」的字段messages 会被 append。"""
calls = int(state.get("llm_calls") or 0) calls = int(state.get("llm_calls") or 0)
if calls >= MAX_LLM_CALLS: if calls >= MAX_LLM_CALLS:
return { return {
@ -103,10 +128,12 @@ class Agent:
messages = _repair_incomplete_tool_calls(messages) messages = _repair_incomplete_tool_calls(messages)
try: try:
# response 可能是普通文字,也可能带 tool_calls只声明要调谁还没执行
response = await self._model.ainvoke(messages) response = await self._model.ainvoke(messages)
return {"messages": [response], "llm_calls": calls + 1} return {"messages": [response], "llm_calls": calls + 1}
except Exception as exc: except Exception as exc:
err = str(exc) err = str(exc)
# 常见 400历史里有 tool_calls 却缺对应的 ToolMessage补占位后再试一次。
if "tool_calls" in err and "tool_call_id" in err: if "tool_calls" in err and "tool_call_id" in err:
try: try:
repaired = _repair_incomplete_tool_calls(messages) repaired = _repair_incomplete_tool_calls(messages)
@ -118,6 +145,7 @@ class Agent:
return {"messages": [AIMessage(content=friendly)], "llm_calls": calls + 1} return {"messages": [AIMessage(content=friendly)], "llm_calls": calls + 1}
async def _tool_node(self, state: MessagesState) -> dict[str, Any]: async def _tool_node(self, state: MessagesState) -> dict[str, Any]:
"""真正执行函数:读上一轮 AIMessage.tool_calls按名字 ainvoke。"""
import asyncio import asyncio
last = state["messages"][-1] last = state["messages"][-1]
@ -141,15 +169,18 @@ class Agent:
observation = f"工具 '{name}' 调用失败: {type(exc).__name__}: {exc}" observation = f"工具 '{name}' 调用失败: {type(exc).__name__}: {exc}"
if not isinstance(observation, str): if not isinstance(observation, str):
observation = str(observation) observation = str(observation)
# tool_call_id 必须对上模型那次声明,否则下一轮 llm_call 会 400。
return ToolMessage(content=observation, tool_call_id=call_id, name=name) return ToolMessage(content=observation, tool_call_id=call_id, name=name)
if len(tool_calls) <= 1: if len(tool_calls) <= 1:
results = [await one(tool_calls[0])] if tool_calls else [] results = [await one(tool_calls[0])] if tool_calls else []
else: else:
# 模型一轮里可能同时要读文件 + 列目录,并行跑。
results = list(await asyncio.gather(*[one(tc) for tc in tool_calls])) results = list(await asyncio.gather(*[one(tc) for tc in tool_calls]))
return {"messages": results} return {"messages": results}
def _should_continue(self, state: MessagesState) -> Literal["tool_node", "__end__"]: def _should_continue(self, state: MessagesState) -> Literal["tool_node", "__end__"]:
"""条件边:有 tool_calls → 去执行;没有 → 本轮结束(纯文字回答)。"""
messages = state.get("messages") or [] messages = state.get("messages") or []
if not messages: if not messages:
return END return END
@ -160,6 +191,11 @@ class Agent:
def _assemble_messages(static: str, dyn: str, history: list[AnyMessage]) -> list[AnyMessage]: def _assemble_messages(static: str, dyn: str, history: list[AnyMessage]) -> list[AnyMessage]:
"""拼出发给模型的完整上下文:固定系统提示 + 动态环境 + 对话历史。
动态部分cwd模型 id每次 llm_call 都重算所以嵌进第一条用户消息
而不是单独一条 SystemMessage避免历史里叠很多条过期系统提示
"""
out: list[AnyMessage] = [SystemMessage(content=static)] out: list[AnyMessage] = [SystemMessage(content=static)]
if not history: if not history:
out.append(HumanMessage(content=dyn)) out.append(HumanMessage(content=dyn))
@ -177,6 +213,7 @@ def _assemble_messages(static: str, dyn: str, history: list[AnyMessage]) -> list
def _strip_images(messages: list[AnyMessage]) -> list[AnyMessage]: def _strip_images(messages: list[AnyMessage]) -> list[AnyMessage]:
"""当前走纯文本接口:用户消息里如果夹了图片块,只留下 text。"""
cleaned: list[AnyMessage] = [] cleaned: list[AnyMessage] = []
for msg in messages: for msg in messages:
if isinstance(msg, HumanMessage) and isinstance(msg.content, list): if isinstance(msg, HumanMessage) and isinstance(msg.content, list):
@ -193,6 +230,11 @@ def _strip_images(messages: list[AnyMessage]) -> list[AnyMessage]:
def _repair_incomplete_tool_calls(messages: list[AnyMessage]) -> list[AnyMessage]: def _repair_incomplete_tool_calls(messages: list[AnyMessage]) -> list[AnyMessage]:
"""补齐「声明了 tool_calls 但没有 ToolMessage」的缺口。
典型场景上次对话做到一半退出checkpoint 里留下 AIMessage.tool_calls
却没有对应结果OpenAI 兼容接口要求每条 tool_call 都有一条同 id ToolMessage
"""
declared: set[str] = set() declared: set[str] = set()
for msg in messages: for msg in messages:
if isinstance(msg, AIMessage) and getattr(msg, "tool_calls", None): if isinstance(msg, AIMessage) and getattr(msg, "tool_calls", None):
@ -228,6 +270,7 @@ def _repair_incomplete_tool_calls(messages: list[AnyMessage]) -> list[AnyMessage
def _map_stream_event(ev: dict[str, Any]) -> AgentEvent | None: def _map_stream_event(ev: dict[str, Any]) -> AgentEvent | None:
"""把 LangGraph 底层事件收成 TUI 认识的几种thinking / text / tool_start / tool_end。"""
kind = ev.get("event") or "" kind = ev.get("event") or ""
meta = ev.get("metadata") or {} meta = ev.get("metadata") or {}
node = meta.get("langgraph_node") node = meta.get("langgraph_node")

Loading…
Cancel
Save