1. 从概念到代码为什么我们需要一个“最小内核”如果你最近在关注AI应用开发尤其是围绕大语言模型LLM构建的智能体Agent那么“Agent Loop”这个词你一定不陌生。它听起来很酷但当你真正想动手实现一个时面对琳琅满目的框架——LangChain、LlamaIndex、AutoGen甚至是新出的Hermes Agent——你可能会感到一丝迷茫。这些框架功能强大但随之而来的是复杂的抽象层、繁多的依赖和陡峭的学习曲线。很多时候我们只是想验证一个想法让一个AI智能体根据我的指令去调用一个工具然后根据结果再决定下一步如此循环直到完成任务。这个“思考-行动-观察”的循环就是Agent Loop的核心。那么有没有一种方式能让我们抛开厚重的框架直接触摸到这个循环的“心脏”呢这就是“最小内核”的意义所在。它不是一个用于生产环境的完整解决方案而是一个教学工具一个理解工具。通过亲手用大约50行代码构建一个最简化的Agent Loop我们能透彻地理解几个关键问题智能体是如何做决策的工具调用是如何被触发的状态是如何在循环中传递和更新的当你理解了这些再去看那些成熟的框架你就会发现它们不过是在这个最小内核之上增加了路由、记忆、并发、容错等“豪华装修”。掌握内核意味着你拥有了“装修”的主动权而不是被框架的设计牵着鼻子走。本文将带你用Python基于一个简单的本地大语言模型比如通过Ollama运行的Llama 3.1 8B构建一个名为harness的最小化Agent内核。我们聚焦于最本质的循环逻辑剥离所有非必要的装饰让你在半小时内看到智能体“动起来”的本质。2. 环境准备与核心组件定义在开始写循环之前我们需要先搭好舞台。这个舞台不需要太复杂但几个关键角色必须到位一个能“思考”的大脑LLM几个能“动手”的工具Tools以及一个管理它们的中枢Agent Core。2.1 搭建你的本地“大脑”Ollama与模型选择首先我们需要一个LLM。为了极致简单和可控我选择在本地运行。Ollama是目前管理本地LLM最方便的工具之一。它的安装非常简单以macOS或Linux为例一行命令即可curl -fsSL https://ollama.com/install.sh | sh安装完成后拉取一个合适的模型。考虑到我们是在做原理验证不需要追求极致的性能一个7B到8B参数量的模型就足够了它们对硬件要求相对友好。我推荐使用Meta的Llama 3.1 8Bollama pull llama3.1:8b这个模型在常识推理和指令跟随方面表现不错足以支撑我们最小内核的演示。运行后Ollama会在本地启动一个API服务默认在http://localhost:11434我们的代码将通过这个API与模型对话。注意确保你的机器有足够的内存建议16GB以上。如果资源紧张可以尝试更小的模型如phi3:mini但指令理解能力会有所下降。2.2 设计工具Tools智能体的“手和脚”智能体自己不会操作世界它需要通过工具。在我们的最小内核里工具就是一个Python函数加上一段能让LLM理解它的描述。我们设计两个最经典的工具一个计算器和一个网络搜索模拟。首先定义工具的接口。一个工具至少需要两个属性name工具名和description给LLM看的描述以及一个_run方法实际执行的函数。class Tool: def __init__(self, name, description, func): self.name name self.description description self._run func def run(self, input_text): 执行工具并返回结果字符串。 try: result self._run(input_text) return str(result) except Exception as e: return fError while running tool {self.name}: {e}接下来实现具体的工具。计算器工具我们使用Python的eval时要极度小心这里仅作演示在实际生产中必须对输入进行严格的过滤和沙箱处理。import math def calculator(query: str) - str: 一个简单的计算器。输入是一个数学表达式字符串例如 3 5 * 2 或 sqrt(16)。 # 警告此处使用eval仅用于演示生产环境必须替换为安全的表达式解析器如ast.literal_eval或第三方库。 allowed_names {k: v for k, v in math.__dict__.items() if not k.startswith(_)} allowed_names.update({abs: abs, round: round}) try: # 极其简化的安全措施实际不可靠 if import in query or __ in query: return Invalid or unsafe expression. result eval(query, {__builtins__: {}}, allowed_names) return str(result) except Exception as e: return fCalculation error: {e} # 创建计算器工具实例 calc_tool Tool( namecalculator, descriptionUseful for performing mathematical calculations. Input should be a valid arithmetic expression like 3 5 * 2 or sin(0.5)., funccalculator )第二个工具我们模拟一个网络搜索。由于我们不想引入真实的网络请求依赖这里用一个固定的字典来模拟返回结果。def simulated_search(query: str) - str: 模拟网络搜索。根据查询返回预设的结果。 knowledge_base { harness: Harness in software engineering often refers to a test harness, which is a collection of software and test data for testing a program unit., agent loop: An Agent Loop is the core execution cycle of an intelligent agent, typically involving steps like Perception, Reasoning, Action, and Learning., openai: OpenAI is an AI research and deployment company, creator of models like GPT-4 and ChatGPT., 今天的天气: 模拟结果北京2023年10月27日晴气温10-18摄氏度西北风3-4级。 } query_lower query.lower() for key, value in knowledge_base.items(): if key in query_lower: return value return fNo specific information found for {query}. Try asking about harness, agent loop, or OpenAI. search_tool Tool( namesearch_web, descriptionUseful for searching general knowledge or current information. Input should be a search query string., funcsimulated_search )现在我们有了两个工具calculator和search_web。它们就是智能体可以使用的“手”。2.3 构建智能体核心Agent Core决策与调度智能体核心是大脑LLM和手Tools之间的协调者。它的主要职责是接收用户指令。构建包含工具信息的提示词Prompt给LLM让LLM决定是直接回答还是使用某个工具。解析LLM的响应。LLM的响应需要被结构化解析以判断它想做什么。通常我们会要求LLM以特定格式如JSON回复。根据解析结果执行相应操作如果LLM决定使用工具就调用对应的工具并将工具返回的结果作为新的上下文再次送给LLM进行“思考”进入下一个循环。如果LLM决定直接回答则循环结束。为了完成步骤3我们需要一个简单的解析器。这里我们约定LLM的响应格式为Action: [工具名] Action Input: [工具的输入内容]或者Final Answer: [最终给用户的答案]我们来编写这个解析函数import re import json def parse_llm_response(response: str): 解析LLM的响应提取 Action 和 Action Input或 Final Answer。 返回一个字典例如 {type: action, tool: calculator, input: 34} 或 {type: answer, output: The result is 7.} response response.strip() # 模式1匹配 Action: ... Action Input: ... action_pattern rAction:\s*(.?)\s*Action Input:\s*(.?)(?:\s*$|\s*Final Answer:) action_match re.search(action_pattern, response, re.DOTALL) if action_match: tool_name action_match.group(1).strip() tool_input action_match.group(2).strip().strip().strip() return {type: action, tool: tool_name, input: tool_input} # 模式2匹配 Final Answer: ... answer_pattern rFinal Answer:\s*(.) answer_match re.search(answer_pattern, response, re.DOTALL) if answer_match: answer answer_match.group(1).strip() return {type: answer, output: answer} # 如果都不匹配默认将整个响应视为最终答案容错处理 return {type: answer, output: response}这个解析器虽然简单但它是连接非结构化文本LLM输出和结构化决策程序逻辑的关键桥梁。在实际的复杂框架中这部分通常通过“输出解析器”Output Parser或要求LLM返回严格的JSON来实现可靠性更高。3. 编织循环实现最简化的 Agent Loop有了大脑、工具和解析器现在我们可以把它们编织成循环了。这个循环的流程正是智能体工作的核心逻辑。3.1 组装提示词Prompt Engineering要让LLM学会使用工具提示词的设计至关重要。我们需要在提示词中清晰地告诉LLM它的角色是什么。它可以使用哪些工具每个工具是干什么的。它应该以什么格式来回应。当前的用户问题是什么。如果之前已经使用过工具那么工具返回的结果是什么这是循环的关键。我们来编写构建提示词的函数def build_prompt(question: str, tools: list[Tool], previous_steps: list[dict] None) - str: 构建给LLM的提示词。 previous_steps 记录之前的动作输入观察三元组。 tools_description \n.join([f- {tool.name}: {tool.description} for tool in tools]) prompt fYou are a helpful AI assistant. You have access to the following tools: {tools_description} To use a tool, you must respond strictly in one of the following two formats: Format 1 (To use a tool): Action: [tool name from the list above] Action Input: [input for the tool] Format 2 (To give the final answer to the user): Final Answer: [your final answer here] The users question is: {question} # 如果之前有步骤把历史加进去这是实现多轮思考的关键 if previous_steps and len(previous_steps) 0: prompt \n\nHere is the history of what has happened so far:\n for i, step in enumerate(previous_steps): prompt fStep {i1}:\n prompt f Action: {step.get(action)}\n prompt f Action Input: {step.get(action_input)}\n prompt f Observation: {step.get(observation)}\n prompt \nBased on the history above, what is your next step?\n else: prompt \nWhat is your first step?\n prompt Remember, you must respond in the specified format.\n return prompt这个提示词模板清晰地定义了任务、工具和响应格式。previous_steps参数是循环的灵魂它让LLM拥有了“记忆”能够基于之前工具执行的结果进行后续推理。3.2 与LLM通信发起一个简单的请求我们需要一个函数来向本地的Ollama服务发送请求获取模型的响应。这里使用requests库。import requests def call_llm(prompt: str, model: str llama3.1:8b) - str: 调用本地Ollama API获取LLM响应。 url http://localhost:11434/api/generate payload { model: model, prompt: prompt, stream: False, options: { temperature: 0.1, # 低温度使输出更确定更遵循格式 num_predict: 512 # 限制生成长度 } } try: response requests.post(url, jsonpayload, timeout60) response.raise_for_status() return response.json()[response] except requests.exceptions.RequestException as e: return fError calling LLM API: {e} except KeyError: return Error: Unexpected response format from LLM API.这里将temperature设得较低0.1是为了让模型的输出更稳定、更倾向于遵循我们要求的格式这对于构建一个可靠的最小循环很重要。3.3 主循环逻辑思考、行动、观察的往复现在将所有部分组合起来形成最终的harness函数也就是我们的Agent Loop最小内核。def harness(question: str, tools: list[Tool], max_steps: int 5) - str: Agent Loop 的最小实现。 question: 用户问题 tools: 可用的工具列表 max_steps: 最大循环步数防止无限循环 返回最终答案字符串。 previous_steps [] # 记录历史(action, action_input, observation) for step in range(max_steps): print(f\n--- Step {step 1} ---) # 1. 思考构建提示词并调用LLM prompt build_prompt(question, tools, previous_steps) print(f[Prompt to LLM]:\n{prompt[:500]}...) # 打印部分提示词便于调试 llm_response call_llm(prompt) print(f[LLM Raw Response]:\n{llm_response}) # 2. 解析LLM的决策 decision parse_llm_response(llm_response) print(f[Parsed Decision]: {decision}) # 3. 行动根据决策类型执行 if decision[type] answer: # 获得最终答案循环结束 final_answer decision[output] print(f\n✅ Loop finished after {step 1} step(s).) return final_answer elif decision[type] action: tool_name decision[tool] tool_input decision[input] # 查找对应的工具 target_tool None for tool in tools: if tool.name tool_name: target_tool tool break if target_tool is None: # 如果LLM指定了一个不存在的工具将其视为观察结果 observation fError: Tool {tool_name} not found. else: # 执行工具 print(f[Executing Tool {tool_name}] with input: {tool_input}) observation target_tool.run(tool_input) print(f[Tool Observation]: {observation}) # 将本次动作输入观察记录到历史中 previous_steps.append({ action: tool_name, action_input: tool_input, observation: observation }) # 继续下一轮循环观察结果会作为下一轮提示词的一部分 else: # 解析失败作为观察错误处理 observation fError: Could not parse LLM response: {llm_response} previous_steps.append({ action: parse_error, action_input: llm_response, observation: observation }) # 如果达到最大步数仍未得到最终答案 return fReached maximum steps ({max_steps}) without a final answer. History: {previous_steps}这个harness函数完美诠释了Agent Loop初始化设定问题、工具和最大步数。循环开始 a.思考Think结合历史构建提示词询问LLM下一步该怎么做。 b.解析Parse解析LLM的响应判断是“行动”还是“直接回答”。 c.行动Act如果是行动则找到对应工具并执行。 d.观察Observe将工具执行的结果记录下来。 e.更新状态将本次行动观察加入历史作为下一轮“思考”的上下文。循环结束条件LLM输出“Final Answer”或达到最大步数。4. 让内核跑起来实战演示与结果分析现在让我们用几行代码启动这个刚刚诞生的智能体看看它如何工作。if __name__ __main__: # 定义可用的工具 my_tools [calc_tool, search_tool] # 测试用例1一个需要计算的问题 question1 请计算一下圆周率π的平方根是多少 print(*50) print(fQuestion: {question1}) answer1 harness(question1, my_tools, max_steps3) print(fFinal Answer: {answer1}) # 测试用例2一个需要搜索的问题 question2 什么是Harness print(\n *50) print(fQuestion: {question2}) answer2 harness(question2, my_tools, max_steps3) print(fFinal Answer: {answer2}) # 测试用例3一个需要多步推理的问题计算后搜索 question3 北京今天的天气适合穿什么衣服先查一下天气再根据气温给建议。 print(\n *50) print(fQuestion: {question3}) answer3 harness(question3, my_tools, max_steps5) print(fFinal Answer: {answer3})运行这段代码你会在终端看到类似以下的输出具体内容因模型随机性略有不同 Question: 请计算一下圆周率π的平方根是多少 --- Step 1 --- [Prompt to LLM]: You are a helpful AI assistant. You have access to the following tools: - calculator: Useful for performing mathematical calculations. Input should be a valid arithmetic expression like 3 5 * 2 or sin(0.5). - search_web: Useful for searching general knowledge or current information. Input should be a search query string. To use a tool, you must respond strictly in one of the following two formats: Format 1 (To use a tool): Action: [tool name from the list above] Action Input: [input for the tool] Format 2 (To give the final answer to the user): Final Answer: [your final answer here] The users question is: 请计算一下圆周率π的平方根是多少 What is your first step? Remember, you must respond in the specified format. ... [LLM Raw Response]: Action: calculator Action Input: math.sqrt(math.pi) [Parsed Decision]: {type: action, tool: calculator, input: math.sqrt(math.pi)} [Executing Tool calculator] with input: math.sqrt(math.pi) [Tool Observation]: 1.7724538509055159 --- Step 2 --- [Prompt to LLM]: ... (提示词包含了Step 1的历史Action: calculator, Action Input: math.sqrt(math.pi), Observation: 1.7724538509055159) Based on the history above, what is your next step? ... [LLM Raw Response]: Final Answer: 圆周率π的平方根大约是1.7724538509055159。 [Parsed Decision]: {type: answer, output: 圆周率π的平方根大约是1.7724538509055159。} ✅ Loop finished after 2 step(s). Final Answer: 圆周率π的平方根大约是1.7724538509055159。对于第二个问题“什么是Harness”智能体可能会选择search_web工具并最终返回我们知识库中预设的定义。最有趣的是第三个问题。智能体需要先使用search_web工具查询“今天的天气”得到模拟的天气信息如“北京晴气温10-18摄氏度”。在下一轮循环中LLM会看到这个观察结果并意识到它已经获得了所需信息从而可以直接给出穿衣建议如“建议穿长袖衬衫加外套”。这个过程完整展示了多步推理和工具链式调用。5. 从内核到框架理解复杂性与扩展方向通过这大约50行代码核心循环部分我们实现了一个能跑通的Agent Loop最小内核。它简陋但完整。现在让我们站在这个内核的肩膀上看看那些成熟的Agent框架如LangChain的AgentExecutor或Hermes Agent到底在做什么。本质上它们是在解决我们这个最小内核暴露出的诸多问题并添加了丰富的功能。5.1 我们内核的局限性脆弱的解析Parsing我们依赖正则表达式来解析LLM的非结构化输出这非常不可靠。LLM稍有偏离格式解析就会失败。成熟框架会使用更鲁棒的方法如输出解析器Output Parsers强制LLM返回JSON等结构化数据或使用更智能的解析库。函数调用Function Calling这是目前的主流方案。直接让LLM生成一个符合特定JSON Schema的函数调用请求这比自然语言格式稳定得多。OpenAI、Anthropic的API都原生支持。有限的记忆Memory我们的previous_steps只是简单的列表将所有历史对话都塞进提示词。这有两个问题上下文长度限制LLM有token数限制历史太长会装不下。信息冗余与焦点丢失所有历史平等地占用篇幅关键信息可能被淹没。 成熟框架会引入各种记忆机制如缓冲记忆Buffer Memory只保留最近N轮对话我们用的就是这种简化版。摘要记忆Summary Memory将长历史总结成一段摘要。向量存储记忆Vectorstore Memory将历史对话嵌入成向量需要时进行语义检索只召回最相关的片段。单一且线性的执行流我们的循环是“思考-行动-观察”的严格顺序。但真实场景可能需要并行工具调用同时查询天气和交通信息。条件分支与规划根据某个工具的结果决定走不同的执行路径。循环与中断处理需要重复尝试或满足条件才退出的任务。 框架通过更复杂的“代理类型”如Plan-and-Execute, ReAct和“工作流引擎”来支持这些。缺乏错误处理与稳定性我们没有处理LLM API调用失败、工具执行超时、无效输入等异常。生产系统必须有完备的重试、降级、超时和回退策略。工具管理的简陋性我们只是简单遍历列表查找工具。框架通常提供工具的统一注册、描述生成、参数验证和依赖注入等功能。5.2 如何基于内核进行扩展理解内核后你可以按需添加功能而不是被框架绑架升级解析器将parse_llm_response函数替换为调用支持Function Calling的模型API或者使用Pydantic来定义严格的输出模型。引入记忆管理创建一个Memory类实现添加、检索、摘要等功能在build_prompt中调用它来构建上下文而非简单拼接所有previous_steps。增加超时与重试在call_llm和tool.run外部包裹try-except和重试逻辑。支持并行执行当解析出多个Action时可以使用asyncio或线程池并发执行工具然后合并结果。添加监控与日志在每个关键步骤决策点、工具调用记录详细的日志便于调试和优化。这个50行的内核就像乐高积木中最基础的那块板。所有的复杂架构都是在这块板之上搭建起来的。亲手实现它最大的收获不是代码本身而是对智能体那种“自主循环”感的深刻理解。下次当你使用LangChain的AgentExecutor时你看到的将不再是一个黑盒而是一个清晰放大的、增强了无数功能的harness循环。这份理解能让你在遇到诡异的问题时更快地定位到是提示词、工具描述、解析逻辑还是记忆管理出了差错从而从框架的使用者转变为真正的驾驭者。