Agent 学习 (3月31日报)
- 学习了agent
Agent 循环
"One loop & Bash is all you need" -- 一个工具 + 一个循环 = 一个智能体。
问题
大模型可以帮你推理代码,但是它不能帮你直接跑测试,生成文件,每一次工具的调用实际上都是你手动把结果粘贴回大模型,在这其中你自己粘贴回去的过程就是循环,为了解决来回复制的这个麻烦问题,我们就有了以下流程
解决方案
+--------+ +-------+ +---------+
| User | ---> | LLM | ---> | Tool |
| prompt | | | | execute |
+--------+ +---+---+ +----+----+
^ |
| tool_result |
+----------------+
(loop until stop_reason != "tool_use")
while stop_reason == "tool_use":
response = LLM(messages, tools)
execute tools
append results
This is the core loop: feed tool results back to the model
until the model decides to stop. Production agents layer
policy, hooks, and lifecycle controls on top.实现
完整的函数如下
def agent_loop(query):
# 用户 prompt 作为第一条消息
messages = [{"role": "user", "content": query}]
while True:
# 将消息和工具定义一起发给 LLM
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
# 追加助手响应。检查 stop_reason -- 如果模型没有调用工具,则结束
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
# 执行每个工具调用,收集结果,作为 user 消息追加,回到第 2 步
results = []
for block in response.content:
if block.type == "tool_use":
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})Tools
加一个工具, 只加一个 handler" -- 循环不用动, 新工具注册进 dispatch map 就行
问题
只有 bash ,所有操作都走 shell,那么就会有以下问题 Agent 如果要查看代码文件,他只能使用 cat 指令
如果要查看的文件有几万行代码,会直接撑爆。 Agent 想要查看文末的 Bug ,但是因为 `s01` 中所写的暴力防撑爆系统(截断前 5000 个字符)无法获取不到文末内容,导致死循环
如果 Agent 想要修改文件里的一行代码,纯 bash 环境下它只能用 sed、awk 或者 echo "代码" > file 代码里充满了引号 "、单引号 '、美元符号 $、反斜杠 \。当大模型试图把这些复杂的代码拼装成一行 sed 命令时,极大概率会写错转义符 导致报错 另外 bash 的权限太大了,很容易导致其他的问题
解决方案
+--------+ +-------+ +------------------+
| User | ---> | LLM | ---> | Tool Dispatch |
| prompt | | | | { |
+--------+ +---+---+ | bash: run_bash |
^ | read: run_read |
| | write: run_wr |
+-----------+ edit: run_edit |
tool_result | } |
+------------------+
The dispatch map is a dict: {tool_name: handler_function}.
One lookup replaces any if/elif chain.实现
每个工具有一个处理函数。路径沙箱防止逃逸工作区
# 接受传入路径,检查路径是否安全
def safe_path(p: str) -> Path:
# 合并路径,计算绝对路径
path = (WORKDIR / p).resolve()
# 防止逃逸出工作区
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
# 优化读取操作
def run_read(path: str, limit: int = None) -> str:
text = safe_path(path).read_text()
lines = text.splitlines()
# 按行数阅读
if limit and limit < len(lines):
lines = lines[:limit]
# 限制字符
return "\n".join(lines)[:50000]dispatch map 将工具名映射到处理函数
# 本质调度中心
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"],
kw["new_text"]),
}循环中按名称查找处理函数。循环体本身与 s01 完全一致
# 遍历 LLM 返回的内容
for block in response.content:
# 过滤工具调用相关内容出来
if block.type == "tool_use":
# 找到对应的处理函数
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler \
else f"Unknown tool: {block.name}"
# 结果打包返回
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})加工具 = 加 handler + 加 schema。循环永远不变
ToDoWrite
"没有计划的 agent 走哪算哪" -- 先列步骤再动手, 完成率翻倍
问题
在多步任务中,模型会丢失任务
因为上下文膨胀会导致系统提示的影响力逐渐被稀释。现在的 LLM 底层都是基于 Transformer 框架的,核心是 注意力机制
- 注意力分配: 当模型阅读上下文时,它会给每一个词分配一个“注意力权重”
- 近因效应(Recency Bias): 模型天生更容易关注最近发生的事情(即对话历史的最末端)
- 稀释效应: 一开始,你的 System Prompt(比如:“你是一个严谨的重构专家,必须按步骤来”)在上下文中很显眼。但当中间塞进了几万字的工具执行日志后,System Prompt 被挤到了极遥远的“古代”。模型在计算注意力分布时,给系统提示词分配的权重变得微乎其微,它“忘了”自己的最初人设 一个 10 步重构可能做完 1-3 步就开始即兴发挥, 因为 4-10 步已经被挤出注意力了
实现原理
# TodoManager 存储带状态的项目。同一时间只允许一个 in_progress
class TodoManager:
def update(self, items: list) -> str:
validated, in_progress_count = [], 0
for item in items:
status = item.get("status", "pending")
if status == "in_progress":
in_progress_count += 1
validated.append({"id": item["id"], "text": item["text"],
"status": status})
if in_progress_count > 1:
raise ValueError("Only one task can be in_progress")
self.items = validated
return self.render()# 模型连续 3 轮以上不调用 `todo` 时注入提醒
if rounds_since_todo >= 3 and messages:
last = messages[-1]
if last["role"] == "user" and isinstance(last.get("content"), list):
last["content"].insert(0, {
"type": "text",
# xml 标签
"text": "<reminder>Update your todos.</reminder>",
})