强军对决
106.2MB · 2026-03-22
Agent 的运行时可以拆解为三个核心机制,它们对应了 Agent 在每一步需要回答的三个问题:
循环控制(Agent Loop) :调用完大模型还要不要继续?→ Workflow Control
输出路由( Output Routing) :LLM 这次输出后应该要做什么?调用工具?还是回答用户?→ Workflow Control
上下文管理(Context Management) :要给模型喂什么信息,该给模型看到什么?→ Context Engineering
总的来说这三者相互协作共同搭建起来一个 Agent 的框架: 上下文管理准备弹药 → LLM 推理 → 输出路由翻译 LLM 的输出结果 → 决策 → 循环控制驱动执行 → 结果回到上下文管理 → 循环继续。
Agent Loop 是 Harness 的心跳 —— 它是由一个 while 循环组成,里面藏着四个很重要的设计决策:
stop_reason = "end_turn"),这也是最常用的判断方式。这些都应该是可配置的,好的 Harness 把它们暴露为参数,比如 Claude Code 就能让我们选择各种执行模式,还能通过 claude --dangerously-skip-permissions 跳过当前会话的所有权限检查,让其自动执行。
该机制主要用来回答一个问题:LLM 这次输出的到底是什么意图? 我们要怎么解析 LLM 输出的结果,进而判断模型要我们接下来做什么呢?
在以前是使用 ReAct 模式,LLM 输出的结果是纯文本,我们通过 prompt 教模型要是输出怎么样的数据格式,然后我们自己用正则从纯文本中“扣”出工具调用,这种实现方式又脆弱又丑陋。例如:
当你需要调用工具时,请用以下格式:
Thought: 我需要搜索一下天气信息
Action: search
Action Input: {"query": "北京天气"}
当你有最终回答时,请用:
Thought: 我已经获得了所有需要的信息
Final Answer: 北京今天晴天,25°C
Thought: 用户想知道北京天气,我需要搜索一下。
Action: search
Action Input: {"query": "东京今天天气"}
Action 跟 Action Input 分别是什么了:import re
match = re.search(r"Action:s*(.+)", response)
input_match = re.search(r"Action Input:s*(.+)", response)
if match:
tool_name = match.group(1).strip() // 调用对应的 tool
args = json.loads(input_match.group(1)) # 很容易崩!
上面 LLM 输出的纯文本可能会存在各种各样的问题:多了一个空格、少了一个引号、把 Action 写成 action 等等,假如正则一解析不出来就有问题了,要么就在 Router 中写大量的兼容逻辑,但是根本枚举不完各种可能存在的错误情况。
而现在各大模型使用的是一种叫 Function Calling 的模式,我明年再调用大模型的 API 接口的时候声明工具的 schema,告诉模型我们现在有哪些工具可用,让模型通过结构化的数据告诉我们需要调用哪些工具,直接输出结构化的 tool_use block,让解析从"猜测"变成了"读取"。
response = client.messages.create(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "东京今天天气怎么样"}],
tools=[
{
"name": "search", // 明确声明有这个 tool
"description": "搜索网络信息",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"}
},
"required": ["query"]
}
}
]
)
2. 模型返回的也是结构化的数据,不是纯文本:
[ { "type": "text", "text": "让我帮你查一下东京的天气。" }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq", "name": "search", "input": {"query": "东京今天天气"} }]
# 不需要正则!直接按类型过滤
tool_calls = [
block for block in response.content
if block.type == "tool_use"
]
if tool_calls:
# 结构化的,名称和参数都是确定的
name = tool_calls[0].name # "search"
args = tool_calls[0].input # {"query": "东京今天天气"}
这种结果是模型训练出来的结果,由模型直接在内部输出结构化的 Json,不需要我们通过 prompt 进行约束,通过参数本身来进行约束,进而可以精确的匹配你声明的工具,在工程的 Router 代码上基本不需要进行任何的容错。
那么模型是怎么知道要触发 Function Calling 模式的呢?答案就是在我们调用 API 传入 tools 参数的时候,模型就会自动进入 Function Calling 模式。
模型输出这里有个重要的参数是 Router 最重要的判断依据,那就是 stop_reason:
tool_use:模型的执行暂停了,等待我们将工具调用结果再喂给模型。Router 的判断就是要继续执行循环了end_turn:模型认为任务已经完成了。Router 的判断就是要退出循环了Function calling 的思想是行业标准,但 JSON 结构各家略有不同:
response.content 中有 type: "tool_use" 的 blockresponse.choices[0].message.tool_calls,arguments 是字符串需 JSON.parseresponse.candidates[0].content.parts 中有 functionCall输出路由层需要做一层适配——这也是 LangChain 等框架存在的原因之一。
虽然 Function Calling 减少了格式错误,但模型偶尔会调用不存在的工具、传入不合理的参数、对危险操作缺乏审慎。验证层从"格式纠错"进化为"语义审查"。
这层验证本身就是一种 Architectural Constraint——用确定性代码防止 Agent 做出危险的工具调用。
另外调用模型传入的 API 列表也不应该全传,工具太多会导致模型选择困难,浪费 Token,增加延迟的,可以适当的增加一些任务动态筛选机制。
总的来说输出路由机制是用来解析模型的输出,判断循环下一步是否要调用工具,是否要退出。而现在有了 function calling 机制让解析模型输出发生了一个质变:
这是 Context Engineering 在 Agent 运行时的具体实现。管理三层信息:
即 system prompt,system message 这部分信息永远都在、永远不变,会包含身份、规则、工具定义等,例如 AGENTS.md/CLAUDE.md 内容。
即对话历史,包含 user/ assistant/ tool message,这部分一直累积后就会膨胀起来,需要做各种策略的管理。
这部分不在消息列表中,但是可以按需取用,例如 Claude.md、向量数据库、完整的 Trace、skill 等知识。 上下文管理的核心职责 = 每次调用 LLM 前,从三层中拼出最优的消息列表。
class ContextManager: # Context Engineering 的实现
def build_messages(self) -> list[dict]:
messages = [{"role": "system", "content": self.system_prompt + self.external_knowledge}]
for msg in reversed(self.full_history):
msg = self._maybe_truncate(msg)
if fits_budget(msg):
messages.insert(1, msg)
return messages
class OutputRouter: # Workflow Control 的实现
def parse(self, llm_response) -> RouterResult:
tool_calls = self._extract_tool_calls(llm_response)
if tool_calls:
self._validate(tool_calls) # Architectural Constraint
return RouterResult(is_final=False, tool_calls=tool_calls)
return RouterResult(is_final=True, content=self._extract_text(llm_response))
class AgentLoop: # Workflow Control 的核心
def run(self, user_input: str) -> str:
self.context.add_user_message(user_input)
for step in range(self.max_steps):
messages = self.context.build_messages() // 加载上下文
response = self.llm.chat(messages) // 调用大模型
result = self.router.parse(response) // 解析大模型的结果
if result.is_final:
return result.content // 结束
for tc in result.tool_calls: // 工具调用
tool_result = self._execute_tool(tc)
self.context.add_tool_result(tc.id, tool_result)
第二课的核心认知:
循环控制 是 Workflow Control 的核心——终止条件、并行度、错误处理、人类介入都是可配置的设计决策
输出路由 把 LLM 的不确定输出转化为确定指令——Function Calling 简化了它,但验证(Architectural Constraint)仍然必要
上下文管理 是 Context Engineering 的运行时实现——三层信息结构,核心挑战是在有限窗口里放进最相关的信息
下一篇进入最容易翻车的领域:工具调用与错误恢复。