Skip to content
Go back

Agent 的本质:LLM + Tool Loop

Agent 的本质:LLM + Tool Loop

一句话结论(30s)

Agent 不是「一次对话」,而是 LLM 反复调用工具、直到满足停止条件才退出的循环。这个循环由三个环节组成:think(模型决定下一步)→ act(运行时执行工具)→ observe(把结果喂回模型),即 ReAct 范式。工程上的真正难点不在循环本身,而在三件事:循环卡死(重复调同一个工具)、token 爆炸(每轮重发全部历史)、结果格式化(工具输出怎么喂回去才不污染上下文)。理解了这三个问题,你就理解了为什么「循环是免费的,工程是昂贵的」。

一、Agent 为什么不是「一次对话」

先看一个普通 LLM 调用:

messages = [{"role": "user", "content": "帮我查一下今天深圳天气"}]
resp = llm.chat(messages)        # 一次往返
answer = resp.content            # 模型直接输出文本

模型只能「凭记忆回答」。如果它不知道深圳今天的天气,就只能编一个——因为它的知识被冻结在训练截止日期,且没有任何能力去访问外部世界。

Agent 的解法是:给模型一个「工具调用」的输出通道。模型不再只能输出文本,还能输出一个结构化的「我要调用这个函数」的意图:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {"id": "call_1", "name": "get_weather", "arguments": "{\"city\": \"深圳\"}"}
  ]
}

运行时(Runtime)收到这个意图后,去真实执行 get_weather("深圳"),拿到 "26°C,晴",再作为一条新消息追加回去:

{"role": "tool", "tool_call_id": "call_1", "content": "26°C,晴"}

然后重新请求模型。这一轮模型看到真实数据后,才输出最终答案「深圳今天 26°C,晴」。

关键洞察就在这里:「调用工具」和「最终回答」不是两种模式,而是同一次生成的不同产物。模型的输出空间被扩展了——它可以输出文字,也可以输出 tool_calls。运行时只需要判断「这次输出里有没有 tool_calls」:有就执行工具、喂回结果、再来一轮;没有就结束。

于是 Agent 的最小实现只有十几行:

def run_agent(user_query, tools, max_iter=20):
    messages = [{"role": "user", "content": user_query}]
    for _ in range(max_iter):                 # ← 循环骨架
        resp = llm.chat(messages, tools=tools)

        if not resp.tool_calls:               # 停止条件:没有工具调用
            return resp.content               # 这就是最终答案

        for call in resp.tool_calls:          # act:执行每个工具
            fn = TOOL_REGISTRY[call.name]
            result = fn(**json.loads(call.arguments))
            messages.append({"role": "assistant", "content": resp.content,
                             "tool_calls": [call]})
            messages.append({"role": "tool",
                             "tool_call_id": call.id,
                             "content": str(result)})   # observe:喂回结果

    raise MaxIterationError("循环未收敛")     # 兜底:迭代上限

这就是一个完整 Agent 的全部骨架。真正的复杂度,都藏在这个 for 循环的工程细节里。

💭 思考:为什么 Agent 的最小实现能短到十几行,真正值钱的却在这十几行之外?—— 一步步推:骨架的职责只有三个——请求模型、看有没有 tool_calls、有就执行并喂回、没有就返回。这十几行是对「循环」这件事的忠实表达,所以短。但一旦放进真实环境,「循环不能无限跑」的约束立刻逼出三层护栏(硬上限、去重、纠偏),「每轮重发历史」逼出 token 预算,工具返回脏数据逼出格式化层。换句话说,骨架短是因为它只描述了「正确的路径」,而工程贵在描述「错误会从哪冒出来、怎么兜住」。如果不加这些,一个绕圈的模型就能把你的账单烧穿——所以「循环是免费的,工程是昂贵的」。

这套十几行的骨架,正是 Claude Code 主循环的最小版本。它的主循环落在 src/query.tsQueryEngine 导入 query 函数并 for await 逐条消费其产出,负责驱动与兜底):每轮请求模型、解析出 tool_use 块,只要有工具调用就执行工具、把结果追加回历史、进入下一圈;没有工具调用就返回最终答案。源码里把停止条件写得非常直白——循环入口声明 let needsFollowUp = false,流式响应里每出现一个 tool_use 块就把它置为 true;一轮流结束后 if (!needsFollowUp) 是唯一的收尾判断(源码注释原话:the sole loop-exit signal)。换言之,你的十几行 Python 和它的差别不在骨架,而在骨架外面套的护栏和可观测性。

二、Tool Loop 的三环节:think → act → observe

ReAct 范式(Yao et al. 2022)把上述循环拆成三个语义明确的环节,每一圈都走一遍。它证明:把「思考」和「行动」交错写进上下文,模型在知识问答、决策类任务上的正确率和可解释性都显著优于纯 Chain-of-Thought 或纯 Act。

环节谁在做做什么产物
think模型看当前上下文,决定下一步动作一段推理 + 一个 tool_call(或最终答案)
act运行时执行模型指定的工具工具的真实返回值
observe运行时 + 模型把返回值格式化后追加回上下文,供下一轮 think一条 role: tool 消息

一圈的时序如下:

用户: "帮我查深圳天气并判断要不要带伞"


┌─────────────────────────────────────────────────┐
│ 第 1 圈                                          │
│  THINK   模型: "需要查天气 → 调 get_weather"      │
│  ACT     运行时: get_weather("深圳") → "26°C,晴"  │
│  OBSERVE 追加 tool 消息,重新请求模型              │
└─────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────┐
│ 第 2 圈                                          │
│  THINK   模型: "26°C 不用带伞 → 直接回答"         │
│          输出不带 tool_calls 的文本               │
│  → 满足停止条件,循环退出,返回最终答案             │
└─────────────────────────────────────────────────┘

注意一个反直觉的点:think 环节的「推理」未必以独立文本形式存在。早期 ReAct 要求模型先输出 Thought: ... 再输出 Action: ...,用纯文本来驱动循环;现代实现(OpenAI function calling、Anthropic tool use)把「行动意图」变成了结构化字段 tool_calls,而「思考」则隐含在模型生成这些字段之前的内部推理里,不再强制暴露。但三个环节的职责没变——只是 act 从「解析文本里的 Action」变成了「解析结构化字段」。

这也是为什么「工具定义」要写得像文档:namedescriptionparameters(JSON Schema)是模型在 think 阶段唯一能看到的关于工具的「说明书」。description 写得含糊,模型就会在 think 阶段调错工具或编造参数。

三、关键工程问题

3.1 循环卡死:重复调同一个工具

最常见的故障是模型陷入死循环——反复调用同一个工具、用同一组参数,期望得到不同结果:

第 3 圈: get_weather("深圳") → "26°C,晴"
第 4 圈: get_weather("深圳") → "26°C,晴"   ← 结果没变,模型还调
第 5 圈: get_weather("深圳") → "26°C,晴"   ← 死循环

根因通常是:模型不理解工具结果,或认为「多调一次能拿到新信息」。工程上必须加护栏,而不是依赖模型自律:

def run_agent(user_query, tools, max_iter=20, repeat_threshold=3):
    messages = [{"role": "user", "content": user_query}]
    seen = {}  # (tool_name, arguments) -> 调用次数

    for step in range(max_iter):
        resp = llm.chat(messages, tools=tools)
        if not resp.tool_calls:
            return resp.content

        for call in resp.tool_calls:
            key = (call.name, call.arguments)
            seen[key] = seen.get(key, 0) + 1
            if seen[key] > repeat_threshold:
                # 强制注入一条提示,逼模型换策略或收尾
                messages.append({"role": "system",
                                 "content": f"已连续 {repeat_threshold} 次调用 "
                                            f"{call.name} 且参数相同、结果未变,"
                                            f"请基于已有信息给出结论或换用其他工具。"})
                continue

            result = TOOL_REGISTRY[call.name](**json.loads(call.arguments))
            messages.append({"role": "assistant", "tool_calls": [call]})
            messages.append({"role": "tool",
                             "tool_call_id": call.id,
                             "content": str(result)})

    raise MaxIterationError(f"{max_iter} 轮未收敛")

三层护栏缺一不可:

  1. max_iter 硬上限——无论什么情况都要能强制退出,这是最后的兜底;
  2. 重复调用检测——(tool_name, args) 作为 key 计数,超阈值就打断;
  3. 注入纠偏提示——不是简单 abort,而是给模型一个「你已经在绕圈了」的信号,让它有机会优雅收尾。

这三层护栏在真实产品里都有对应:max_iter 硬上限,就是 Claude Code 的 maxTurns——src/query.tsif (maxTurns && nextTurnCount > maxTurns) 直接返回一个 max_turns_reached 附件,QueryEngine 再把它翻译成 error_max_turns 错误;外加一个 maxBudgetUsd 预算上限,src/QueryEngine.tsif (maxBudgetUsd !== undefined && getTotalCost() >= maxBudgetUsd) 一超就返回 error_max_budget_usd。这两个开关「无论模型绕不绕圈」都一定会触发,是最终兜底。值得玩味的是,(tool, args) 去重这种细粒度的「重复调用检测」,在 Claude Code 主循环里反而没有显式实现——它防卡死主要靠 turn 上限 + 预算上限,再叠加可恢复错误的重试次数上限:例如输出 token 超限后的恢复循环,src/query.ts 里硬编码了 MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3,最多重试 3 次。这印证了「护栏是工程,不是模型自律」:产品更愿意用无差别的硬上限兜底,而不是逐工具计数。

💭 思考:为什么防卡死必须靠「护栏」,而不是靠「模型自律」?—— 反推:模型陷入死循环,恰恰是因为它在 think 阶段「相信」再调一次能拿到新信息——它已经在自律了,自律得出的结论是「继续调」。所以指望模型自己停,等于指望一个以为自己没绕圈的人承认自己在绕圈。于是约束就落到运行时这一侧:你必须能在模型之外观察到「它在绕圈」,并用无差别的硬上限(max_iter、预算)兜底——因为它们不依赖模型「听不听话」,无论绕不绕都触发。这也解释了为什么 Claude Code 用 turn 上限 + 预算上限,而不是逐工具计数:逐工具计数要维护状态、要判断「什么叫参数相同」,成本高;而硬上限对任何死循环都一视同仁、必然生效。护栏的本质,是把「正确性」从概率事件(模型自律)变成确定事件(运行时强制)。

3.2 Token 爆炸:每轮都在重发历史

Tool Loop 的致命代价是:每一轮都要把从第一条 user 消息到当前的全部历史重新发给模型。上下文是自回归的,模型没有「增量记忆」,它只能靠重读历史来维持状态。

量化一下:假设单轮对话平均 N token,循环跑 M 轮,则总 token 消耗是:

总 token ≈ N × M(历史重发) + 每轮新增的工具结果

也就是 O(M × N) 的线性膨胀。一个只查一次天气的简单任务,实际账单可能是普通单轮对话的 5-10 倍。工具返回一个 10KB 的 JSON(比如查了一次日志、读了一个大文件),这个 10KB 会在之后每一轮都被重发一遍

缓解手段按优先级排:

手段思路落地
结果裁剪observe 阶段就截断,别让脏数据进上下文工具结果先 truncate 到预算内再喂回
上下文压缩循环过程中把旧历史总结成摘要Claude Code 的 /compact 与自动压缩,压成摘要再续跑
prompt caching不变的前缀只算一次钱Anthropic 的 cache_control、OpenAI 的 prompt caching
子任务隔离把大任务拆成多个独立循环,避免单个循环无限膨胀多 Agent / 子循环各带独立上下文

其中结果裁剪性价比最高,却最常被忽视。工具函数的返回值不是「原样」塞回去就完事——这是下一节的内容。

自动压缩也不是「把历史删掉」那么粗。OpenAI Codex 的 compact 模块(codex-rs/core/src/compact.rs)把它落成一种叫 Memento 的策略(分析埋点里记作 CompactionStrategy::Memento):把旧历史压成一段带 SUMMARY_PREFIX 前缀的摘要(format!("{SUMMARY_PREFIX}\n{summary_suffix}")),同时从尾部倒序保留最近若干条用户消息——build_compacted_history_with_limituser_messages.iter().rev() 倒推,approx_token_count 估算每条长度,truncate_text 按 token 截断,总预算硬控在 COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000(20,000 token)以内,超出部分截断;压缩完还会硬编码弹一条警告(WarningEvent):「长线程、多次压缩会降低模型准确度,能开新线程就开新线程」。所以压缩是 token 爆炸的止损阀,但它自己也有精度代价——不是免费的。

💭 思考:为什么 token 爆炸的第一优先是「结果裁剪」,而不是更高级的压缩或缓存?—— 从成本与位置反推:爆炸的源头是「工具返回的脏数据被原样塞进历史,然后每轮重发」。结果裁剪发生在 observe 环节、也就是数据「刚进门」的那一步——在源头截断,等于后面每一轮重发省下的都是同一份。而压缩是「事后止损」,缓存是「不变前缀只算一次钱」,它们各有代价:压缩有精度损失(Codex 的 Memento 自己都弹警告),缓存只能省不变部分。所以按「性价比 = 省下的 token / 引入的复杂度」排,源头截断第一。如果不先裁剪,一个 2MB 的 read_file 结果会在之后每一轮都被重发一遍——后面的压缩和缓存都得先替它扛这个锅。

3.3 工具结果如何格式化

observe 环节最容易被写成 content: str(result) 一行带过,但它是整个循环里最脏的环节。三种典型坏情况:

① 结果太长——grep 返回 5000 行、read_file 读了 2MB 文件。原样塞回去会瞬间撑爆上下文,把模型注意力冲散。

def observe(result, budget=4000):
    text = json.dumps(result, ensure_ascii=False) if not isinstance(result, str) else result
    if len(text) > budget:
        return text[:budget] + f"\n...[已截断,共 {len(text)} 字符]"
    return text

② 结果报错——工具执行抛异常(网络超时、权限不足)。如果直接 except 吞掉,模型会以为调用成功了,基于一个空结果继续推理,越跑越偏。正确做法是把错误也转成结构化文本喂回去

try:
    result = fn(**args)
    content = {"ok": True, "data": result}
except Exception as e:
    content = {"ok": False, "error": f"{type(e).__name__}: {e}"}
messages.append({"role": "tool", "tool_call_id": call.id,
                 "content": json.dumps(content, ensure_ascii=False)})

让模型看到 "ok": false,它才能在下一轮 think 里做出「换一个工具」或「告诉用户失败了」的正确决策。

③ 结果不是给模型看的——工具返回了一个中间产物(比如一个文件的路径、一个临时 ID),模型不需要全文,只需要一个「引用句柄」。这时 observe 应该返回「摘要 + 句柄」,而不是把原始对象塞进去。

一句话总结:observe 环节的产物不是「工具结果」,而是「喂给模型的下一条 prompt」。它需要像写 prompt 一样精心裁剪、标注状态、控制长度。

这套「裁剪 + 标状态 + 控长度」在 Claude Code 里落成了一个专门的工具结果存储层:src/constants/toolLimits.ts 里定义了 DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000(默认 50,000 字符)作为单结果上限,每个工具声明自己的 maxResultSizeChars——GrepTool 20,000、BashTool 30,000、AgentTool 100,000,而读文件的 FileReadTool 直接声明 Infinity(永不落盘,否则会形成 Read→文件→Read 的死环);消息层再套一个 applyToolResultBudgetsrc/utils/toolResultStorage.ts)的「每消息聚合预算」(MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200_000)。真正的大结果不是截断,而是「落盘换句柄」——把超大的工具结果持久化到磁盘,只把 PREVIEW_SIZE_BYTES = 2000(前 2000 字节)的预览(Preview (first ...):)和文件句柄喂回模型,模型需要时再用 Read 工具去读全文。这正是③「摘要 + 句柄」的产品级实现。

💭 思考:为什么 observe 的产物不能是「工具结果」,而必须是「下一条 prompt」?—— 从「谁在消费这份产物」反推:工具结果是给运行时看的,但 observe 喂回去的对象是模型——模型要在下一轮 think 里基于它做决策。所以这份产物要满足的是「模型读起来最容易做对决策」:太长会冲散注意力(要截断)、报错要标状态(让模型知道失败了,而不是基于空结果继续推理)、中间产物只给句柄(模型要的是引用不是全文)。如果不这么设计——比如异常被 except 吞掉——模型会以为调用成功,基于一个空结果越跑越偏。所以 observe 不是「转发」,是「翻译 + 裁剪 + 标注」,它写的是 prompt,不是日志。

四、从裸循环到工程化

4.1 可观测性:给循环每一圈埋探针

前面三节的护栏,都建立在一个前提上:你能看到循环里发生了什么。但裸 for 循环是个黑盒——Agent 跑完 20 圈,你只看到最终答案,中间哪一圈绕了路、哪个工具把方向带偏了,完全无从追溯。所以工程化的第一步不是写护栏,而是先让每一圈变得可见

这正是 Claude Code 的 hooks(钩子) 系统在解决的问题。它允许你在工具调用的前后挂自定义脚本,其中最相关的两个是:

对照循环结构看得很清楚:PreToolUse 是 act 的闸门,PostToolUse 是 act 的记录。有了这两者,一次 Agent 运行就从「只有最终答案」变成了「可以逐圈回放」:

{"tool": "Read",  "args": {"file_path": "..."}, "ok": true, "latency_ms": 120}
{"tool": "Grep", "args": {"pattern": "..."},   "ok": true, "latency_ms": 45}
{"tool": "Grep", "args": {"pattern": "..."},   "ok": true, "latency_ms": 43}
{"tool": "Grep", "args": {"pattern": "..."},   "ok": true, "latency_ms": 41}

第二条到第四条 Grep 参数几乎一样——从日志里一眼就能看出模型在绕圈,这正是 3.1 节「重复调用检测」想自动拦截的模式,而日志让它先变得可见。

顺带一提,工具本身也在被标准化:MCP(Model Context Protocol) 把「工具」定义成外部可发现、可插拔的服务,模型不需要把函数写死在运行时里,而是通过一个标准协议去连接外部的工具服务器。它解决的问题和 hooks 互补——hooks 管「调用前后做什么」,MCP 管「工具本身怎么定义和接入」。

4.2 显式状态机:慢工具、可失败、有副作用的场景

对于「慢的、可能失败的、有副作用的」工具(例如发通知、创建工单、部署到生产、需要等待人工审批),一个裸 for 循环就不够用了:这些动作中间可能横跨几秒甚至需要人确认,还可能出现「失败重试」「等待中」这些中间态,裸循环只能用 continue 和一堆 flag 硬凑。更好的做法是落成一个显式的有限状态机

                    ┌──────────────┐
        start ─────▶│    THINK     │
                    └──────┬───────┘
                           │ 产出 tool_call

                    ┌──────────────┐
              ┌────▶│     ACT      │──失败/重试──┐
              │     └──────┬───────┘            │
              │            │ 成功               │
              │            ▼                    │
              │     ┌──────────────┐            │
              └─────│   OBSERVE    │            │
                    └──────┬───────┘            │
                           │ 追加结果、重新请求模型

                    ┌──────────────┐
                    │  有 tool_call│───是──▶ THINK(下一圈)
                    └──────┬───────┘
                           │ 否(最终答案)

                    ┌──────────────┐
                    │     DONE     │
                    └──────────────┘

对应的核心代码骨架:

class StatefulAgent:
    def __init__(self, tools, max_iter=15, repeat_threshold=3):
        self.tools = tools
        self.max_iter = max_iter
        self.repeat_threshold = repeat_threshold

    def run(self, task: str):
        state = State.THINK
        messages = [{"role": "user", "content": task}]
        history = []  # (tool, args) 去重计数,防绕圈

        while state != State.DONE:
            if state == State.THINK:
                resp = llm.chat(messages, tools=self.tools)
                if not resp.tool_calls:
                    state = State.DONE            # 停止条件:模型给出最终结论
                    return resp.content
                self.pending = resp.tool_calls
                state = State.ACT

            elif state == State.ACT:
                for call in self.pending:
                    if self.is_loop(history, call):      # 3.1 防绕圈
                        messages.append(self.correction(call))
                        state = State.OBSERVE
                        break
                    result = self.safe_execute(call)     # 3.3 错误转文本
                    messages.append(as_assistant_msg(call))
                    messages.append(as_tool_msg(call, self.format(result)))
                else:
                    state = State.OBSERVE
                    continue

            elif state == State.OBSERVE:
                state = State.THINK                     # 回到 think,进入下一圈

        raise MaxIterationError("循环未收敛")

和裸 for 循环相比,状态机的收益是:

  1. 中间态显式化——ACT 里的「失败重试」「等待人工确认」不再需要用 continue 和 flag 硬凑,而是清晰的状态迁移;
  2. 可插桩——每个状态入口都能埋日志/指标,THINK 耗时、ACT 成功率、OBSERVE 触发频次分开统计,出问题能定位到具体环节;
  3. 可测试——状态迁移逻辑可以脱离真实 LLM 单独测,用 mock 的 tool_call 序列验证「绕圈时是否真的会注入纠偏提示」。

这类场景还有一个特殊约束放大了这些工程问题:工具的副作用不可撤销get_weather 查十次无所谓,但「发送通知」「创建工单」是有副作用的动作——循环卡死导致的重复调用,意味着用户被重复通知、工单被重复创建。所以 3.1 的重复检测在这里从「性能优化」升级成了「正确性要求」:有副作用的工具必须在 act 前做幂等或去重。这也是为什么 PreToolUse 的「闸门」角色在有副作用的场景里尤其重要——它把「是否允许执行」这个判断,从循环内部挪到了执行之前。

总结

Agent 的本质一句话:LLM + Tool Loop——模型输出「要调用什么工具」,运行时去执行,结果喂回去,如此往复直到模型给出不带工具调用的最终答案。

理解了这个循环,再看市面上的 LangChain、AutoGPT、Claude Code、Cline,本质上都是在同一个 while not done 骨架外,套上不同的工具注册表、护栏和可观测性。循环是免费的,工程是昂贵的。

章末提问

追问 1:Agent 和「一次 LLM 调用」的本质区别是什么?

结论先行:Agent 不是一次对话,而是一个「LLM 反复调用工具、直到满足停止条件才退出」的循环。

因为:模型一次调用只能输出文本或 tool_call 意图;是运行时判断「有没有 tool_calls」、执行工具、把结果喂回、再请求模型,如此往复。「调用工具」和「最终回答」是同一生成通道的两种产物,不是两种模式。停止条件只有一个——这一轮输出不再带 tool_calls。

追问 2:Tool Loop 怎么保证收敛,而不是烧 token 死循环?

结论先行:靠三层护栏——max_iter 硬上限兜底、(tool, args) 重复调用检测、注入纠偏提示。

因为:死循环的根因是模型误以为「再调一次能拿新信息」,靠模型自律停不下来。所以必须在运行时强制:硬上限保证无论如何能退出,重复检测识别「同参数同工具」的绕圈,纠偏提示不是简单 abort 而是给模型一个「你在绕圈」的信号。真实产品里 Claude Code 用 maxTurns + maxBudgetUsd 这种无差别硬上限兜底,印证了「护栏是工程,不是模型自律」。

追问 3:为什么 Tool Loop 会有 O(M×N) 的 token 爆炸,怎么缓解?

结论先行:因为模型无状态,每一轮都要全量重发历史,N token 跑 M 轮就是 O(M×N);缓解按性价比排是结果裁剪、上下文压缩、prompt caching、子任务隔离。

因为:上下文是自回归的,没有增量记忆,只能靠重读历史维持状态。一个 10KB 的工具结果会在之后每一轮被重发。所以第一优先是在 observe 源头截断(结果裁剪),成本最低收益最高;压缩(摘要)是止损阀但有精度代价;缓存只省不变前缀;子任务隔离是把大循环拆成多个独立小循环。

追问 4:慢的、可失败的、有副作用的工具,为什么不能塞进裸 for 循环?

结论先行:因为这类工具需要中间态(失败重试、等待人工确认)和幂等保护,裸循环只能用 flag 硬凑,正确做法是升级成显式状态机。

因为:裸 for 循环只有「请求—执行—喂回」一条主路径,遇到「等待中」「重试」这些中间态没有表达空间;更关键的是副作用不可撤销——循环卡死导致的重复调用意味着用户被重复通知、工单被重复创建,重复检测从「性能优化」升级成「正确性要求」。状态机把 THINK/ACT/OBSERVE 的迁移显式化,可插桩、可测试,且让 PreToolUse 的「闸门」在 act 之前拦下危险操作。


Share this post on:

Previous Post
二进制思维——从毒药白鼠到分金块的编码艺术
Next Post
AI Agent 工程地图——从 LLM 到可编程 Agent