nanoAgent进化版:agent-plus

之前说过这个精简版的agent还缺少了几样功能:缺少的功能
作者也认识到了这个问题,于是就有了 nanoAgent第二个版本:agent-plus.py,它多了几十行代码,却解决了两个根本问题:1. agent有了记忆,2. 让agent能够规划

如果不知道讲什么请看 上一篇: 百行代码认识Agent:nanoAgent解读

看看多了什么?

能力 agent agent-plus 是否变化
工具调用 3个工具 3个工具 ❌无变化
单任务执行 ❌无变化
跨会话记忆 每次运行都失忆 记忆文件 ✔️有变化
任务规划 无规划 先拆解再执行 ✔️有变化
多步串联 任务步骤间共享messages context ✔️有变化

记忆系统:最朴素的实现方案

记忆的存储:使用markdown文件

MEMORY_FILE = "agent_memory.md"

def save_memory(task, result):
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    entry = f"\n## {timestamp}\n**Task:** {task}\n**Result:** {result}\n"
    try:
        with open(MEMORY_FILE, 'a') as f:
            f.write(entry)
    except:
        pass

agent-plus使用了最简单朴素的方案:把历史任务和结果追加写入到一个markdown文件中。没有存入数据库,也没有使用向量索引,就是纯文本。每次任务执行完成,agent就把 “何时、做了什么、得到的结果” 追加到文件尾端。

记忆的恢复:滑动窗口机制

def load_memory():
    if not os.path.exists(MEMORY_FILE):
        return ""
    try:
        with open(MEMORY_FILE, 'r') as f:
            content = f.read()
            lines = content.split('\n')
            return '\n'.join(lines[-50:]) if len(lines) > 50 else content
    except:
        return ""

agent-plus 只取最近50条历史记录。这是一个简单的滑动窗口策略,因为记忆是无限增长的,但LLM的上下文窗口是有限制的,所以获取最近的N条记忆。

记忆的延申:将其注入 system prompt

def run_agent_plus(task, use_plan=False):
    memory = load_memory()
    system_prompt = "You are a helpful assistant that can interact with the system. Be concise."
    if memory:
        system_prompt += f"\n\nPrevious context:\n{memory}"
    messages = [{"role": "system", "content": system_prompt}]

恢复出来的记忆被拼接到 system prompt末尾,以 Previous context形式注入到新的prompt,这样LLM在处理新任务时就能"感知"到之前发生了什么。

记忆的本质

这个方案看起来原始粗糙,但揭示了一个根本性问题:LLM本身没有持久记忆,所有“记忆”是通过在prompt中注入历史消息来实现的。 目前的大模型都遵从这种模式,只是在 存在哪、怎么找、找多少上有不同的实现方式。

规划系统:让agent先think再do

为什么需要规划?

想象如果把整个任务丢给 LLM,让它在循环中自行摸索。简单任务没问题,但面对复杂任务(比如"重构整个项目"),LLM 容易迷失在细节中,忘记全局目标。

agent-plus引入了一个可选项-规划


def create_plan(task):
    print("[Planning] Breaking down task...")
    response = client.chat.completions.create(
        model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
        messages=[
            {"role": "system", "content": "Break down the task into 3-5 simple, actionable steps. Return as JSON array of strings."},
            {"role": "user", "content": f"Task: {task}"}
        ],
        response_format={"type": "json_object"}
    )
    try:
        plan_data = json.loads(response.choices[0].message.content)
        if isinstance(plan_data, dict):
            steps = plan_data.get("steps", [task])
        elif isinstance(plan_data, list):
            steps = plan_data
        else:
            steps = [task]
        print(f"[Plan] {len(steps)} steps created")
        for i, step in enumerate(steps, 1):
            print(f"  {i}. {step}")
        return steps
    except:
        return [task]

规划的设计细节

  • 使用LLM做规划:让LLM把任务拆解为 3~5个可执行的步骤,以json array的形式返回
  • 结构化输出:response_format={“type”: “json_object”} 强制LLM返回json格式
  • 优雅降级:如果json解析失败,则将整个任务作为一个步骤执行,这里使用了防御性编程,这在agent开发中很重要。

agent和agent-plus两种执行范式对比

每个step中仍然是初代agent的工作方式

初代agent

1.思考

2.行动

3.观察

规划(全局思考)

step1

step2

step3

agent-plus

多步执行:步骤之间上下文传递

def run_agent_step(task, messages, max_iterations=5):
    messages.append({"role": "user", "content": task})
    actions = []
    for _ in range(max_iterations):
        response = client.chat.completions.create(
            model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
            messages=messages,
            tools=tools
        )
        message = response.choices[0].message
        messages.append(message)
        if not message.tool_calls:
            return message.content, actions, messages
        for tool_call in message.tool_calls:
            function_payload = getattr(tool_call, "function", None)
            if function_payload is None:
                continue
            function_name = str(getattr(function_payload, "name", ""))
            raw_arguments = str(getattr(function_payload, "arguments", ""))
            function_args = parse_tool_arguments(raw_arguments)
            print(f"[Tool] {function_name}({function_args})")
            function_impl = available_functions.get(function_name)
            if function_impl is None:
                function_response = f"Error: Unknown tool '{function_name}'"
            elif "_argument_error" in function_args:
                function_response = f"Error: {function_args['_argument_error']}"
            else:
                function_response = function_impl(**function_args)
                actions.append({"tool": function_name, "args": function_args})
            messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": function_response})
    return "Max iterations reached", actions, messages
  • 变化1:messages从内部创建变为外部传入,这样多个步骤间就可以共享同一个对话历史。
  • 变化2:返回值中包含messages;函数把更新后的消息列表返回给调用方供下一步使用

编排: 把步骤串联起来

all_results = []
for i, step in enumerate(steps, 1):
    if len(steps) > 1:
        print(f"\n[Step {i}/{len(steps)}] {step}")
    result, actions, messages = run_agent_step(step, messages)
    all_results.append(result)
    print(f"\n{result}")
final_result = "\n".join(all_results)
save_memory(task, final_result)

可以看到,前一个步骤的输出作为下一个步骤的输入,每个步骤只完成一件事最后输出最终结果。

其中每个步骤内的 messages是短期记忆列表,而 agent_memory.md保存了长期记忆。

记忆类型 载体 生命周期 实现
短期记忆 messages列表 单次运行内 步骤之间共享的对话历史
长期记忆 agent_memory.md 跨任务 save_memory函数

记忆方案的局限性和优化方向

nanoAgent的记忆方案简单粗暴可成为“最小可行性记忆”,但还是不够智能,可以继续优化;

  • 记忆无差别截断–>向量数据库+语义检索: 如果只保留最近N行信息可能会丢失重要的早期信息,更好的方案是把记忆存入向量数据库,每次加载记忆时只检索与当前语义相关的记忆。这是RAG的核心思路
  • 无记忆压缩–>记忆蒸馏:当记忆超过阈值时,用LLM自动压缩旧记忆——提取关键信息,丢弃细节。
  • 全量注入prompt–>记忆作为工具:不把记忆强塞进 system prompt,可提供一个 search_memory工具,agent按需查询,agent决定什么时候需要回忆以及回忆什么。
  • 单层记忆–>多层记忆:将记忆细分为 工作记忆(当前任务)、短期记忆(最近几次对话的摘要内容)、长期记忆(压缩后的关键事实);每层的信息密度和保留时间不同。

agent-plus总结

plus版本的agent有了记忆和规划能力,但是还有一些问题需要解决

  • 工具是硬编码的:无法接入外部服务,tools market
  • 没有行为约束:不同项目,不同团队对 agent要求不同,无法定制agent
  • 规划是被动触发的:用户通过 --plan 参数来激活规划功能,agent不知道什么时候该规划

这三个问题,引出了agent架构中最精彩的三个概念:MCP(工具协议)、Skills(技能)、Rules(行为规则)

下篇-从nanoAgent看OpenClaw/Trae/Claude Code底层原理

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐