1. 从零上手 Agent 开发:为什么“两天入门”不是吹牛
这两年 AI Agent 这个词被喊得震天响,但真正动手写过的人都知道,从“知道 Agent 是什么”到“能跑起来一个像样的 Agent”,中间隔着一道不浅的沟。我见过太多人卡在第一步:环境装了一堆,框架文档翻了几十页,结果连一个能正常调用工具的 Agent 都跑不起来。问题不在于笨,而在于大多数教程要么太浅——只告诉你 Agent 等于“大模型加工具调用”,要么太深——上来就讲多智能体协作和状态机编排,中间那段“怎么把第一个能用的东西搭出来”被跳过了。
这篇要聊的 Harness 电子小册子,恰好补的就是这一段。它不是什么官方文档的翻译,也不是学术论文的科普版,而是一份面向动手派的实战手册。核心思路很直接:用最短的路径让你理解 Agent 的骨架,然后通过几个可运行的最小示例,把“感知—决策—执行—反馈”这个循环真正跑通。适合谁看?如果你已经会写 Python,对大模型 API 调用不陌生,但还没亲手搭过一个完整的 Agent 工作流,那这份东西就是给你准备的。两天时间,不是让你成为专家,而是让你跨过“从零到一”那道坎,后面再学 LangChain、DeepAgents 这些框架时,心里有底。
我拿到这份小册子之后,花了两个晚上完整过了一遍,又自己动手把里面的例子改了几个版本。下面就把我的理解、实操记录和踩过的坑,按模块拆开讲清楚。
2. 核心思路拆解:Harness 到底在解决什么问题
2.1 Agent 开发的真正门槛在哪里
很多人以为 Agent 开发的门槛是“调 API”,其实不是。调 API 只是最表层的东西,真正让人卡住的是三件事:第一,不知道一个 Agent 系统应该由哪些模块组成,脑子里没有架构图;第二,不知道工具调用是怎么被触发的,模型输出和实际执行之间那层“胶水”怎么写;第三,不知道状态怎么管理,多轮对话里上下文怎么传递、怎么截断、怎么持久化。
Harness 这份小册子的价值就在于,它把这三件事拆得很细。它没有一上来就讲 LangChain 的 Chain 和 AgentExecutor,而是先用最原始的方式——直接调模型 API、手动解析输出、手动路由到工具函数——让你看清楚每一步在干什么。等你把这个“裸写”的版本跑通了,再去看 LangChain 的封装,就会有一种“哦,原来它帮我做了这些”的感觉。这种从底层往上走的学习路径,比直接学框架要扎实得多。
2.2 为什么选 DeepSeek 作为默认模型
小册子里默认用的是 DeepSeek 的 API,这个选择很务实。原因有几个:一是 DeepSeek 的 API 兼容 OpenAI 的调用格式,你之前如果写过 OpenAI 的代码,改个 base_url 和 model 名字就能跑;二是它的价格在同类模型里属于很能打的,做实验的时候不用心疼 token;三是它在函数调用和结构化输出上的表现,对于入门级 Agent 来说完全够用。
当然,你也可以换成别的模型。小册子里也提了,如果你手头有 Claude 或者别的模型的 API,改配置就行。但我的建议是,第一遍跟着走的时候别换,先用 DeepSeek 把流程跑通,减少变量。等你理解了整个链路,再去做模型对比实验,那时候换模型才有意义。
2.3 Harness 和 Agent 的关系:别被名字绕晕
热词里有个问题问“harness 和 agent 区别”,这个问题其实挺关键的。简单说,Agent 是那个“会思考、会决策、会调用工具”的主体,而 Harness 是包裹在 Agent 外面的那层“脚手架”或者说“测试台”。它负责给 Agent 提供运行环境、注入工具、管理状态、记录日志、处理异常。你可以把 Agent 想象成一个司机,Harness 就是那辆车——司机决定往哪开,但车提供了方向盘、油门、刹车和仪表盘。
小册子里对 Harness 的定义更偏向“工程化实践”:它不只是个运行容器,还包括了一套开发范式——怎么定义工具、怎么组织 prompt、怎么处理模型返回的中间状态、怎么在失败时重试。这些东西在写 demo 的时候可以糊弄过去,但一旦你要做一个能稳定跑起来的 Agent 应用,就绕不开。
3. 环境准备与核心依赖:十分钟把架子搭起来
3.1 Python 环境与依赖安装
小册子建议用 Python 3.10 以上,我实测 3.11 和 3.12 都没问题。虚拟环境用 venv 或者 conda 都行,我个人习惯用 conda,因为后面如果要装一些科学计算相关的包,conda 的依赖解析更省心。
核心依赖其实很少,主要就是openai这个包——因为 DeepSeek 兼容 OpenAI 的接口,所以直接用官方 SDK 就行。另外还需要python-dotenv来管理 API Key,rich用来在终端里打印带格式的日志,方便调试。如果你要跑小册子里的完整示例,可能还需要requests和pydantic。
pip install openai python-dotenv rich pydantic requests这里有个小坑:openai包的版本更新很快,不同版本之间 API 有细微差异。小册子里的代码是基于 1.x 版本写的,如果你装的是 0.x 版本,调用方式完全不一样。建议直接装最新版,然后对照小册子里的代码检查一下client.chat.completions.create这个调用路径。
3.2 API Key 配置与第一个连通性测试
拿到 DeepSeek 的 API Key 之后,别急着写 Agent,先写个最简单的脚本确认能调通。这一步很多人会跳过,结果后面出问题了不知道是模型的问题还是自己代码的问题。
import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com" ) response = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "user", "content": "用一句话解释什么是 AI Agent"} ] ) print(response.choices[0].message.content)跑通这个脚本,说明你的网络、API Key、SDK 版本都没问题。如果报错,大概率是三个原因:API Key 没配好、base_url 写错了、或者账户余额不足。小册子里特别提醒了一句:DeepSeek 的 base_url 是https://api.deepseek.com,不要在后面加/v1,加了反而会 404。这个细节很多教程都没提,我第一次用的时候就踩了这个坑。
3.3 项目目录结构建议
小册子给了一个很清爽的目录结构,我稍微调整了一下,更适合自己扩展:
agent-harness/ ├── .env ├── main.py ├── tools/ │ ├── __init__.py │ ├── weather.py │ └── calculator.py ├── harness/ │ ├── __init__.py │ ├── runner.py │ └── logger.py └── prompts/ └── system.txt把工具、Harness 核心逻辑、prompt 模板分开存放,后面加新工具或者改 prompt 的时候不会乱。特别是当你的工具数量超过五个之后,全堆在一个文件里会非常痛苦。
4. 核心模块实操:从裸写 Agent 到 Harness 封装
4.1 第一步:手动实现一个最简 Agent 循环
小册子最精彩的部分,就是它先让你不用任何框架,纯手写一个 Agent 循环。这个循环的逻辑是:把用户输入和系统 prompt 发给模型,模型返回一个“我想调用某个工具”的指令,你解析这个指令,执行对应的函数,把结果再发给模型,模型根据结果生成最终回复。
听起来简单,但手写一遍你会发现很多细节问题。比如模型返回的工具调用指令格式不固定,有时候是 JSON,有时候是带 markdown 代码块的 JSON,有时候还会在 JSON 前后加一堆解释性文字。小册子里给了一个很实用的解析策略:先用正则把 JSON 部分抠出来,再用json.loads解析,如果失败就重试或者让模型重新生成。
import json import re def extract_tool_call(text): # 尝试匹配 markdown 代码块中的 JSON pattern = r'```(?:json)?\s*(\{.*?\})\s*```' match = re.search(pattern, text, re.DOTALL) if match: return json.loads(match.group(1)) # 尝试直接匹配 JSON 对象 pattern = r'\{[^{}]*"tool"[^{}]*\}' match = re.search(pattern, text, re.DOTALL) if match: return json.loads(match.group(0)) return None这个函数我后来改了好几版,加上了对嵌套 JSON 的处理和对转义字符的兼容。小册子里的版本更简单,但作为入门够用了。关键是你要理解:模型输出是不可控的,你的解析代码必须足够健壮。
4.2 工具定义与注册机制
Agent 能做什么,取决于你给它什么工具。小册子里用了一个很轻量的注册机制:每个工具就是一个 Python 函数,加上一个描述字符串,然后用一个字典把工具名映射到函数和描述上。
TOOLS = {} def register_tool(name, description): def decorator(func): TOOLS[name] = { "function": func, "description": description } return func return decorator @register_tool("get_weather", "查询指定城市的当前天气,输入参数为 city") def get_weather(city: str): # 实际实现会调用天气 API return f"{city}今天晴,气温 25 度"这个设计的好处是,你加新工具的时候只需要写一个函数加一个装饰器,不用改 Harness 的核心代码。小册子特别强调了一点:工具的描述要写得像给模型看的“使用说明书”,把参数类型、参数含义、返回值格式都说清楚。描述写得越清楚,模型调用工具的成功率越高。
我实测下来,工具描述里加上“输入参数为 xxx”这种明确提示,比只写“查询天气”效果好很多。另外,工具函数的参数最好都用类型注解,虽然 Python 不强制,但模型看到类型注解之后,生成的参数格式会更规范。
4.3 Harness 运行器的核心逻辑
Harness 运行器是整个小册子的核心。它负责管理对话历史、调用模型、解析工具调用、执行工具、把结果回传给模型,直到模型不再请求工具调用为止。
class AgentHarness: def __init__(self, client, model, system_prompt, max_turns=10): self.client = client self.model = model self.system_prompt = system_prompt self.max_turns = max_turns self.history = [] def run(self, user_input): self.history.append({"role": "user", "content": user_input}) for turn in range(self.max_turns): response = self.client.chat.completions.create( model=self.model, messages=[{"role": "system", "content": self.system_prompt}] + self.history ) message = response.choices[0].message self.history.append(message) tool_call = extract_tool_call(message.content or "") if not tool_call: return message.content tool_name = tool_call.get("tool") tool_args = tool_call.get("args", {}) if tool_name not in TOOLS: self.history.append({ "role": "user", "content": f"错误:工具 {tool_name} 不存在,请重新选择" }) continue result = TOOLS[tool_name]["function"](**tool_args) self.history.append({ "role": "user", "content": f"工具 {tool_name} 的执行结果是:{result}" }) return "达到最大轮次限制,任务未完成"这段代码有几个关键点值得展开。第一,max_turns是必须的,防止模型陷入死循环。我见过模型反复调用同一个工具、每次都得到相同结果、然后继续调用的案例,没有轮次限制的话会一直烧 token。第二,工具执行失败的时候,不要把异常直接抛出去,而是把错误信息作为“工具结果”回传给模型,让模型自己决定下一步怎么办。第三,历史记录会越来越长,后面需要做截断或者摘要,小册子里提了但没展开,我在下一节会补充。
4.4 系统 Prompt 的设计要点
小册子里给了一个系统 prompt 的模板,我把它拆解了一下,核心要素有四个:角色定义、工具列表、输出格式要求、行为约束。
角色定义要明确告诉模型“你是一个能调用工具的助手”,而不是“你是一个聊天机器人”。工具列表要把所有可用工具的名称、描述、参数格式列出来,最好用 JSON Schema 的格式。输出格式要求要明确规定:当需要调用工具时,输出一个包含tool和args字段的 JSON;当不需要调用工具时,直接输出自然语言回复。行为约束包括:不要编造工具执行结果、不要调用不存在的工具、如果工具执行失败要尝试其他方案。
我自己的经验是,系统 prompt 里加上一两个 few-shot 示例,效果会明显提升。示例不用多,一个“调用工具”的例子加一个“直接回复”的例子就够了。模型看到具体格式之后,输出会稳定很多。
5. 常见问题与排查技巧实录
5.1 模型不调用工具,直接编造答案
这是最常见的问题。你明明给了工具,模型却不用,直接根据自己的知识回答。原因通常是系统 prompt 里没有强调“必须使用工具获取实时信息”,或者工具描述不够吸引人。
解决办法:在系统 prompt 里加一句“对于需要实时数据的问题,你必须调用工具,不能依赖你的内部知识”。另外,把工具描述写得更具体,比如“查询天气”改成“查询指定城市当前时刻的天气状况,包括温度、湿度、风力”。我实测下来,描述越具体,模型调用意愿越强。
5.2 工具调用参数格式错误
模型返回的 JSON 里,参数名写错了,或者参数类型不对。比如工具定义的是city: str,模型返回的是{"city_name": "北京"}。这个问题很难完全避免,但可以通过两个手段降低概率:一是在工具描述里明确写出参数名和类型,二是在 Harness 里加一层参数校验,如果参数不对就返回错误信息让模型重试。
import inspect def validate_args(func, args): sig = inspect.signature(func) try: sig.bind(**args) return True, None except TypeError as e: return False, str(e)这个校验函数很轻量,但能拦住大部分参数格式问题。
5.3 对话历史过长导致 token 超限
多轮对话之后,历史记录会越来越长,最终超过模型的上下文窗口。小册子里提了三种策略:滑动窗口、摘要压缩、关键信息提取。我推荐滑动窗口加摘要的组合:保留最近 N 轮完整对话,更早的对话用模型生成一个摘要,把摘要作为系统消息的一部分。
def compress_history(history, keep_recent=6): if len(history) <= keep_recent: return history old_messages = history[:-keep_recent] recent_messages = history[-keep_recent:] summary_prompt = "请用一段话总结以下对话的核心内容:\n" for msg in old_messages: summary_prompt += f"{msg['role']}: {msg.get('content', '')}\n" # 调用模型生成摘要 summary = call_model(summary_prompt) return [ {"role": "system", "content": f"之前的对话摘要:{summary}"} ] + recent_messages这个方案的好处是保留了长期记忆的“精华”,同时控制了 token 消耗。缺点是摘要本身也要消耗 token,所以keep_recent的值要调好,我一般设 6 到 8 轮。
5.4 工具执行超时或异常
工具函数执行时间过长,或者抛出了未捕获的异常,会导致整个 Agent 卡住。解决办法是给工具执行加上超时控制和异常捕获。
import signal class TimeoutError(Exception): pass def timeout_handler(signum, frame): raise TimeoutError("工具执行超时") def safe_execute(func, args, timeout=10): signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout) try: result = func(**args) signal.alarm(0) return result except Exception as e: signal.alarm(0) return f"工具执行出错:{str(e)}"注意signal.alarm只在 Unix 系统上有效,Windows 上需要用threading或者multiprocessing来实现超时。小册子里用的是concurrent.futures,兼容性更好,但代码稍微复杂一点。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 模型不调用工具 | prompt 未强调工具使用 | 检查系统 prompt | 加“必须调用工具”约束 |
| 参数格式错误 | 工具描述不清晰 | 检查工具描述 | 明确参数名和类型 |
| token 超限 | 历史记录过长 | 打印历史长度 | 滑动窗口加摘要 |
| 工具执行卡住 | 无超时控制 | 检查工具函数 | 加超时和异常捕获 |
| 模型输出无法解析 | 输出格式不稳定 | 打印原始输出 | 加 few-shot 示例 |
| API 调用报错 | Key 或 base_url 错误 | 检查 .env 配置 | 确认 base_url 不加 /v1 |
6. 从 Harness 到 LangChain:什么时候该换框架
6.1 手写 Harness 的边界在哪里
手写 Harness 适合学习和做原型,但一旦你的 Agent 要处理复杂任务,就会遇到瓶颈。比如你需要多个 Agent 协作、需要条件分支和循环、需要持久化状态、需要接入向量数据库做检索增强,这些手写起来会非常痛苦。小册子里也说了,Harness 的目的是让你理解原理,不是让你用它做生产系统。
我自己的判断标准是:如果你的 Agent 工具超过 10 个,或者需要多轮复杂决策,或者需要和外部系统做深度集成,那就该考虑上框架了。LangChain 和 DeepAgents 是目前比较主流的选择,前者生态成熟,后者在 Agent 编排上更专注。
6.2 LangChain 的 DeepAgents 现在能力如何
热词里有人问“langchain 的 deepagents 现在的能力咋样”,我最近也花时间试了一下。DeepAgents 的核心卖点是“深度 Agent”,它支持更复杂的任务分解和工具编排,内置了规划、执行、反思的循环。和手写 Harness 相比,它帮你处理了状态管理、错误重试、工具路由这些脏活。
但说实话,DeepAgents 的文档还在完善中,有些 API 变动比较快。如果你现在要上手,建议先跟着官方示例跑一遍,别急着改。和 Claude 的 Agent 能力相比,DeepAgents 在工具调用的稳定性上还有差距,但胜在开源和可定制。如果你用的是 DeepSeek 作为底层模型,DeepAgents 的兼容性还不错,基本能跑通。
6.3 从 Harness 迁移到 LangChain 的实操建议
如果你已经跟着小册子把 Harness 写了一遍,迁移到 LangChain 会很快。核心概念是一一对应的:Harness 里的工具注册对应 LangChain 的Tool类,Harness 里的运行循环对应AgentExecutor,Harness 里的系统 prompt 对应PromptTemplate。
迁移的时候注意两点:一是 LangChain 的工具描述格式和手写的不一样,需要用@tool装饰器重新定义;二是 LangChain 的 Agent 类型有好几种,入门先用zero-shot-react-description,等熟悉了再试openai-tools或者structured-chat。
7. 两天入门之后:下一步该往哪走
7.1 把 Agent 接入真实工具链
小册子里的工具都是模拟的,下一步就是接入真实 API。我建议从两个方向入手:一是接入一个真实的天气 API 或者搜索 API,体验一下网络请求和错误处理;二是接入一个数据库查询工具,体验一下参数化查询和结果格式化。这两个方向覆盖了大部分实际场景。
接入真实工具的时候,安全是个大问题。特别是数据库查询工具,一定要做参数校验和权限控制,不能让模型生成的 SQL 直接执行。小册子里提了一句“工具执行要有沙箱”,但没展开,这个后面需要专门花时间研究。
7.2 并发与性能:Agent 怎么扛住多用户
热词里有人问“ai agent 怎么扛并发”,这个问题在入门阶段可以先放一放,但心里要有数。单机跑一个 Agent 很简单,但要同时服务多个用户,就需要考虑异步、队列、状态隔离这些问题。我的建议是,先用 FastAPI 把 Agent 包成一个 HTTP 服务,然后用asyncio处理并发请求。每个请求创建独立的 Harness 实例,避免状态串扰。
from fastapi import FastAPI from pydantic import BaseModel import asyncio app = FastAPI() class Query(BaseModel): user_id: str message: str @app.post("/agent") async def run_agent(query: Query): harness = AgentHarness(client, "deepseek-chat", SYSTEM_PROMPT) result = await asyncio.to_thread(harness.run, query.message) return {"result": result}这个方案能扛住小规模并发,但生产环境还需要加限流、缓存、监控。这些内容小册子没覆盖,属于进阶话题。
7.3 我踩过的几个坑和对应建议
第一个坑是 API Key 泄露。我一开始把 Key 硬编码在代码里,后来改成环境变量,再后来用.env文件加.gitignore。建议你从第一天就用.env,别偷懒。
第二个坑是模型输出不稳定。同一个 prompt,有时候模型返回 JSON,有时候返回 markdown,有时候返回纯文本。我的解决办法是在系统 prompt 里加严格的格式要求,并且在解析失败时自动重试一次。重试的时候把上一次的输出也带上,告诉模型“你上次的输出格式不对,请严格按照 JSON 格式重新输出”。
第三个坑是工具描述写得太随意。我一开始写“查询天气”,模型经常不调用。改成“查询指定城市的当前天气状况,输入参数为 city(字符串,如'北京'),返回温度、湿度和天气描述”之后,调用成功率明显提升。
第四个坑是忘记设置max_turns。有一次测试的时候模型陷入了死循环,反复调用同一个工具,十分钟烧掉了几块钱的 token。虽然钱不多,但这是个坏习惯。任何 Agent 循环都必须有退出条件。
7.4 关于学习路径的个人体会
两天入门是可行的,但前提是你每天至少投入三到四个小时,而且要有 Python 基础。如果你完全没写过代码,两天可能只够你把环境装好。我的建议是,第一天跟着小册子把裸写 Harness 跑通,第二天尝试改几个地方:加一个新工具、改一下系统 prompt、换一个模型试试。改的过程比读的过程学到的东西多得多。
另外,别一上来就追求“多智能体协作”或者“自主规划”这些高级概念。先把单 Agent 加工具调用这个最基本的模式吃透,后面学什么框架都会很快。我见过太多人跳过基础直接上 LangGraph,结果连状态怎么传递都没搞明白,调 bug 调到怀疑人生。
最后分享一个小技巧:在 Harness 里加一个日志模块,把每一轮的模型输入、模型输出、工具调用、工具结果都打印出来。调试的时候看日志,比在代码里到处加 print 高效得多。小册子里用rich做了彩色日志,我后来换成了结构化日志,存成 JSON 文件,方便后续分析。
这个方向后续还可以往 RAG 加 Agent 的组合走,也就是让 Agent 能检索外部知识库再回答问题。那是另一个话题了,等把基础打牢再说。