最近在折腾 Agent 项目,发现圈子里讨论最多的几个词就是 DeepSeek-Harness、harness anything、skill 编排。踩了一圈坑之后我最大的体会是:搭 Agent 开发环境,难的不是装框架,而是怎么让环境"可靠"——可复现、可调试、可回归。这篇文章我想用一个大家最熟悉的业务模块——Android 登录模块,完整走一遍用 Harness 搭 Agent 开发环境的流程。读完你能直接照着搭出自己的一套环境,并且学到我实测下来最有价值的那些细节。
这套方案适合这几类人:准备把 Agent 落到真实业务里的后端/客户端工程师,被各种 Agent 框架绕晕想找轻量方案的开发者,以及想在团队里推广"环境即代码"的工程负责人。我会尽量用大白话讲清楚每个环节为什么这么做,而不是只丢一堆配置。
1. Agent 开发环境为什么需要框架,先搞清楚 Harness 的定位
1.1 从裸调大模型到带工具箱的 Agent
先回想一下最原始的"调模型写代码"方式:你拿着 API Key,写一段 Python 脚本,把用户的输入拼进 Prompt,等模型吐一段 JSON 回来。这种方式做个小 Demo 没问题,但一旦业务逻辑复杂起来,立刻会遇到三个坎。
第一是大模型本身没有记忆,每次对话都要重新把上下文全塞进去;第二是大模型不会主动调用外部系统,你想让它帮你发个验证码、查个订单,它只能"口嗨";第三是输出格式不稳定,模型今天返回这种 JSON,明天可能换个结构,你的业务代码跟着遭殃。
Agent 框架要解决的就是这三件事:管理会话上下文、提供工具调用能力、把模型输出约束成可靠结构。Harness 这类轻量级框架做的正是这件事,而且它做得比大型全家桶更"克制"——不绑架你的项目结构,只给你 Agent 运行时最核心的骨架。
1.2 Harness 想解决的几个核心问题
社区里叫 Harness 的项目不止一个,但设计理念高度一致:让大模型在一个受控环境里安全地调用工具、完成任务。我理解它的核心抽象就四个:Skill(技能)、Tool(工具)、Runtime(运行时)、Session(会话)。
Skill 是一组能力的封装,比如"发送短信验证码"是一个技能,"校验验证码"是另一个技能;Tool 是 Skill 落地成可执行函数后的最小单元,对应你代码里一个具体的方法;Runtime 负责调度模型和工具之间循环,相当于 Agent 的心脏;Session 则承载每一次对话的上下文。
和 LangChain、CrewAI 这类"重框架"相比,Harness 最大的特点是"只做编排,不做业务"。它不会帮你封装登录逻辑,也不会替你决定业务流程,它只提供一个让模型"思考-调用-观察-再思考"的循环。这个循环越简单越透明,出问题的时候越容易排查。
1.3 为什么拿 Android 登录模块当第一个试点
选试点业务是有讲究的。我当时列了几个候选:登录模块、购物车模块、推荐流模块。最后选了登录,原因有三个。
第一,登录模块边界极其清晰。一个手机号验证码登录,流程就是:发起验证码、校验验证码、创建会话、返回令牌。状态流转固定,不会出现"这个需求到底算不算登录"这种模糊地带。
第二,登录模块有天然的确定性校验闭环。验证码是否正确、手机号格式是否合法,这些都是非黑即白的判断。Agent 在这种场景里本来就不应该"自由发挥",它要做的只是按规则调用工具、把结果反馈给用户。这种"确定性逻辑 + LLM 决策"的分界,恰好是 Agent 工程化最理想的起点。
第三,登录模块牵涉安全和权限,能逼你把环境的可靠性做扎实。你会自然地去考虑工具权限、日志脱敏、失败重试这些在生产环境里必须面对的问题,而不是停留在玩具 Demo 层面。
| 候选模块 | 边界清晰度 | 确定性校验 | 工程挑战 | 是否适合首试点 |
|---|---|---|---|---|
| 登录模块 | 高 | 高 | 中(安全和权限) | 非常适合 |
| 购物车模块 | 中 | 中 | 低 | 一般 |
| 推荐流模块 | 低 | 低 | 高 | 不建议 |
2. 环境初始化:搭一套能稳定复现的 Harness 开发环境
2.1 前置依赖与版本选择
先说结论,我实测下来这套环境需要的依赖非常少:Python 3.10 以上、git、uv 或者 poetry 任一即可,Docker 可选。不需要单独的 Android 环境——这个要划重点:我们这里的 Agent 开发环境跑在本地或者服务器上,Android 登录模块在这里是以接口契约和 Mock 服务的形式出现的。你想连真机也可以,但第一个版本用 Mock 接口完全够用,并且能帮你排除掉一堆设备相关的干扰因素。
Python 版本强烈建议锁定 3.10 或 3.11。我试过在 3.9 上装某个版本的 Harness,依赖解析直接报错;3.12 虽然能装上,但个别原生依赖编译需要额外的构建工具链,小白容易卡住。用 3.10/3.11,踩坑成本最低。
2.2 拉取 Harness 并安装
安装过程我给你一个可以直接抄的流程。假设项目目录叫android-login-agent:
mkdir android-login-agent && cd android-login-agent git clone https://github.com/deepseek-ai/DeepSeek-Harness.git harness cd harness uv sync --extra dev harness --version这里有两个容易踩的坑。第一,uv 同步依赖的时候尽量用--extra dev,否则后面跑测试和调试工具的时候会报缺模块;第二,不建议直接pip install到全局环境,一个 Agent 项目通常要配多个版本的依赖,用 uv 或者 poetry 隔离环境能帮你省掉后面大量"为什么我这里跑不起来"的困扰。
装完框架之后需要配置模型供应商。Harness 默认支持 OpenAI 兼容协议,所以接 DeepSeek 只需要把 API Key 写进环境变量:
export DEEPSEEK_API_KEY=sk-xxxx export HARNESS_MODEL=deepseek-chat注意:密钥永远不要写进项目文件。我见过不止一个同事把 key 直接贴在 config.yaml 里,结果仓库一同步就等于公开了密钥。正确做法是写进
.env文件,并且把.env加入.gitignore。
2.3 初始化工程骨架
我的建议是不要一上来就铺一层很深的目录,保持简单,后续按需生长。一个跑通登录 Demo 的骨架差不多长这样:
android-login-agent/ ├── harness/ # Harness 框架本身 ├── skills/ │ ├── send_login_code/ # 发送验证码技能 │ ├── verify_login_code/ # 校验验证码技能 │ └── create_user_session/ # 创建会话技能 ├── mocks/ # 登录服务的 Mock 实现 ├── tests/ │ ├── fixtures/ # 测试固定数据 │ └── test_login_flow.py ├── agent.yaml # Agent 编排配置 ├── .env.example # 环境变量模板 └── pyproject.toml # 项目依赖骨架定好之后,你需要建立两条铁律:一是锁定版本。框架版本、模型版本、依赖版本都写死,不然今天跑通明天挂掉,因为你不知道哪个上游依赖偷偷变了行为;二是 Mock 服务要跟真实接口契约一致。接口字段名、状态码、错误结构都要按真实 Android 端对接的协议来定义,否则后面验收集成的时候会发现"Agent 是对的,是 Mock 跟真实服务长得不一样"。
3. 把 Android 登录模块拆成 Agent 能指挥的"技能"
3.1 先把登录流程抽象成状态机
动手写代码之前,先把业务逻辑画清楚。手机号验证码登录看起来简单,但它其实是一个典型的有限状态机:初始状态 -> 等待验证码 -> 校验中 -> 会话建立 -> 已登录。任何一步失败都会回到初始状态或者进入异常分支。
把这个状态机画出来有两个好处。第一,它能帮你看清楚哪些环节是确定性的:手机号格式校验、验证码比对、会话生成,这些写死就行;第二,它能帮你定位 LLM 应该站在哪一层做决策:模型负责听懂用户意图、选择合适的技能、在失败时决定下一步策略,而不是去决定"验证码到底对不对"。
3.2 Skill 与 Tool 的边界怎么切
这是 Agent 工程化里最核心也最容易混乱的点。我的原则很简单:涉及安全、状态变更、资金、隐私的操作,全部做成确定性 Tool,不给模型自由发挥的空间;而需要理解意图、组合多步操作的流程,分配给 LLM 决策。
拿登录模块举例,我最终切的 Tool 列表是这样:
| 技能名称 | 底层 Tool | 确定性 | 说明 |
|---|---|---|---|
| 发送验证码 | send_login_code | 高 | 手机号格式校验、频控、超时 |
| 校验验证码 | verify_login_code | 高 | 密文比对,只返回成功/失败 |
| 创建议题 | create_user_session | 高 | 生成 token,绑定 session |
| 查询登录状态 | get_login_status | 高 | 给 Agent 一个"确认当前状态"的抓手 |
细看你会发现,所有 Tool 都是确定性执行、结构化返回的,没有一个需要模型"猜"。模型要做的只是判断"用户想登录、需要先发验证码、验证码对不对、下一步该调什么"。
3.3 实现一个验证码 Skill
说了这么多,上一个实际可用的代码片段。用 Harness 约定写一个校验验证码的 Skill:
# skills/verify_login_code/skill.py from pydantic import BaseModel, Field import harness class VerifyLoginCodeInput(BaseModel): phone: str = Field(description="用户手机号") code: str = Field(description="短信中的验证码") request_id: str = Field(description="发送验证码时返回的请求标识") @harness.tool(name="verify_login_code", description="校验短信验证码是否正确") def verify_login_code(input_data: VerifyLoginCodeInput) -> dict: # 真实项目里这里改成 auth-service 的 HTTP 调用 # 这里只写核心逻辑,防止验证码被重复使用 stored = mock_store.get(input_data.request_id) if stored is None: return {"ok": False, "error": "request_id 不存在或已过期"} if stored.verified: return {"ok": False, "error": "验证码已被使用,请重新发送"} if stored.code != input_data.code: return {"ok": False, "error": "验证码错误"} stored.verified = True return {"ok": True, "phone": input_data.phone}这个实现看起来很简单,但它埋了三层可靠性设计。
第一,验证码一次性。用verified标记保证同一个验证码只能用一次,这是登录安全的基本要求。第二,即使校验失败,返回数据也是结构化 JSON,模型能直接读懂并决定下一步动作,不需要它去"理解"一堆含糊的报错。第三,入参用 pydantic 做了强校验,字段描述写清楚,这样模型生成参数的时候更不容易出错。
实操心得:写 Skill 的时候,每个返回字段都要考虑"模型能不能看懂"。宁可多返回一个 error 字段,也别让模型去猜失败原因。我在早期版本里试过只返回
{"ok": false},结果模型反复重试同一个错误操作,把验证码都耗光了。
4. 选择模型与编排:把技能串成一个能跑的 Agent
4.1 注册工具与技能
写好了 Skill,接下来要把它们注册进 Agent。Harness 通常支持两种方式:一种是装饰器自动发现,另一种是在配置文件里显式声明。项目大了之后我推荐显式声明,因为自动发现虽然省事,但调试的时候你根本不知道哪些工具被加载进来了。
我的agent.yaml长这样:
agent: name: android-login-agent model: deepseek-chat temperature: 0.0 system_prompt: | 你是 Android 登录助手,负责帮用户完成手机号验证码登录。 严格按以下流程执行: 1. 用户提供手机号后,调用 send_login_code 发送验证码。 2. 用户提供验证码后,调用 verify_login_code 校验。 3. 校验通过后,调用 create_user_session 创建会话。 4. 任何一步失败,直接返回错误原因,不要重试超过一次。 tools: - send_login_code - verify_login_code - create_user_session - get_login_status session: ttl: 30m这里有几个参数是我的经验之谈。temperature: 0.0是必须的,登录流程不需要任何创造性,温度归零才能保证同样输入尽量同样输出。system_prompt里加了"不要重试超过一次",这能有效防止模型在验证码错误的时候无限循环调用。
4.2 把校验结果作为硬约束回传
编排环节最容易忽略的一点是:Tool 返回结果必须成为后续决策的硬约束,而不是参考信息。比如verify_login_code返回{"ok": false, "error": "验证码错误"},模型的下一个动作就应该是"告诉用户验证码错误并停下来",而不是自作主张换个验证码再试。
要做到这一点,除了在系统提示词里写清楚,还有一个更狠的办法:在 Tool 返回里加retryable字段。当某个工具返回{"ok": false, "retryable": false}时,运行时就该强制终止当前工具循环;只有retryable: true才允许模型重新规划。
这套机制相当于给模型装了一道刹车片。实测下来,加上这个约束之后,登录流程的失败重试次数直接降了一半多。
4.3 跑通一次完整的登录闭环
环境配好、技能注册完,终于到了最激动人心的时刻——跑第一个完整流程。我在终端里执行命令,看到这样的输出:
[00:00:00.124] user: 用 138****8888 登录一下 [00:00:00.873] llm: 用户提供了手机号,先发送验证码 [00:00:00.912] tool: send_login_code(phone="138****8888", channel="sms") [00:00:01.401] tool: return {"ok": true, "request_id": "req_2025_0001"} [00:00:01.402] llm: 验证码已发送,请用户查收 [00:00:04.211] user: 验证码是 482913 [00:00:04.890] llm: 调用 verify_login_code 校验 [00:00:04.923] tool: verify_login_code(request_id="req_2025_0001", code="482913") [00:00:05.087] tool: return {"ok": true, "phone": "138****8888"} [00:00:05.120] llm: 验证通过,创建会话 [00:00:05.188] tool: create_user_session(phone="138****8888") [00:00:05.402] tool: return {"token": "eyJhbGciOi...", "expires_in": 604800} [00:00:05.410] agent: 登录成功,会话已建立这个日志的价值在于:你可以完整看到"用户输入 -> 模型决策 -> 工具调用 -> 结果返回 -> 下一步决策"的全过程。所谓 Agent 环境可靠,本质就是这条链路上的每个环节都可观测、可回放、可复现。如果哪天用户反馈登录失败,你只要把当次 Session 的日志拉出来,就能准确指出是哪一步出了问题。
5. 可靠性不只是不崩:日志、回放、测试与容错
5.1 全链路日志与 Trace
很多 Agent 项目跑起来像黑盒,出了问题只能干瞪眼。我的经验是:从第一天起就要把链路日志做全。每个工具调用都要记录入参、出参、耗时、失败原因;每次 LLM 请求都要记录模型、token 数、延时;整个 Session 要有唯一的 trace id 串起来。
具体落地时我会给每个工具加一个简单的装饰器,统一做埋点:
import time import logging logger = logging.getLogger("harness.trace") def traced_tool(func, name): def wrapper(*args, **kwargs): start = time.time() try: result = func(*args, **kwargs) logger.info("tool=%s args=%s result=%s cost=%.0fms", name, kwargs, result, (time.time() - start) * 1000) return result except Exception as exc: logger.error("tool=%s args=%s error=%s", name, kwargs, exc) raise return wrapper有了这份日志,排查问题的效率能提升一个量级。特别是当模型突然"抽风"调用了一个不存在的工具参数时,你能清清楚楚看到它到底生成了什么。
5.2 用 Mock 服务和固定种子做回归测试
LLM 是概率模型,Agent 环境最大的敌人就是"这次能跑通下次跑不通"。为了对抗这种不确定性,我强烈建议把"变量"和"不变量"分开。变量的部分是模型的输出,不变量的部分是工具的行为。工具的行为要固化——这正是 Mock 服务的主场。
我做了两套 Mock:一套是日常开发用的"快乐路径" Mock,所有调用都返回成功;另一套是回归测试用的"场景化" Mock,固定返回预置数据,比如verify_login_code永远返回"验证码错误"。
配合固定种子环境,跑回归测试时,每次测试用的手机号、验证码都是固定的。这样当你修改某个 Skill 之后,只要跑一遍测试套件,就能确认没有破坏已有的登录流程。
下面是回归测试的核心逻辑:
def test_login_flow_success(): result = agent.run("用 138****8888 登录,验证码是 482913") assert result["status"] == "logged_in" assert result["steps"] == ["send_login_code", "verify_login_code", "create_user_session"]这里只校验 Agent 完整走到了登录成功,并校验了工具调用顺序。硬编码顺序可能会比较脆弱,但对第一个版本来说,明确固定比灵活更重要。
5.3 常见问题与排查技巧实录
在搭这套环境的过程中,我踩过的坑和网上大家讨论最多的问题,整理了一张速查表:
| 现象 | 原因 | 排查与解决 |
|---|---|---|
| 模型反复调用同一个工具 | 返回错误信息不够清晰,或缺少 retryable 约束 | 检查工具返回结构,增加终止条件 |
| 工具入参缺字段 | Skill 描述不清楚,模型不知道要传 phone | 在 pydantic Field 里写清楚每个字段含义 |
| 验证码被重复发送 | 频控逻辑没放在 Tool 层 | 发送验证码的 Skill 里必须做限频 |
| 环境依赖总是冲突 | 没有锁定版本 | 用 uv/poetry 锁版本,CI 里用同一锁文件 |
| 日志脱敏不到位 | 手机号、token 直接打进日志 | 加统一的脱敏过滤器,只保留前后三位 |
这里重点说第一个问题。模型反复调用同一个工具,往往不是模型笨,而是你的返回信息不足以让它做决策。比如验证码错误,你不能只返回{"ok": false},而要返回{"ok": false, "retryable": false, "error": "验证码错误,请重新获取验证码"}。模型看到明确的错误语义和下一步建议,才会走正确分支。
异常分支一定要实测。快乐路径跑通了不算完成,验证码错误、会话过期、手机号格式错误这三个分支至少要覆盖一遍,否则上线就是事故。
5.4 安全与审计,登录模块绕不开的责任
登录模块碰的是账号体系,安全问题不能等到上线前才想。我的建议是三件套:权限最小化、日志脱敏、操作审计。
权限最小化指的是 Agent 运行时只拥有它完成任务所需的最小权限。验证码 Skill 只允许读——不允许改手机号绑定关系;会话创建 Skill 只允许生成 token——不允许修改用户资料。日志脱敏则是所有日志输出前,手机号中间四位打码、token 截断。操作审计更进一步,把 Agent 每次工具调用都落库,保存调用者、时间、入参出参摘要,出问题时能追溯。
这三件事看起来繁琐,但正因为目标是登录模块,你才会被逼着把安全地基打牢。否则后面扩展到支付模块,再补安全设计就晚了。
6. 从登录到全模块:这套环境的扩展路径
6.1 沉淀可复用的模块模板
登录模块跑通后,最大的收获不是登录本身,而是一套可以复用到其他模块的模板。我总结下来,每个 Agent 化业务模块的模板包含五个部分:模块状态机定义、工具清单与描述、Mock 服务契约、回归测试用例、环境配置文件。
有了这个模板,第二个模块——比如注册模块——的搭建成本会大幅下降。因为你只需要重新定义状态机和工具清单,环境、测试框架、日志规范全是现成的。
6.2 团队协作与迭代节奏
环境即代码这件事,一个人用是技术,团队用是工程。我建议从第一天就引入版本管理和评审流程:Skill 改动必须走 PR,每次合并前自动跑一遍 Agent 回归测试;模型升级、依赖升级单独做,不能和业务改动混在一起。
迭代节奏上,我的经验是"小步快跑,单点收口"。一次只改一个 Skill、增加一个工具,跑通后立刻固化测试。这套环境我最看重的,就是能让我对这个 Agent 的行为有很强的掌控感——每次改动都有测试兜底,不会出现改了一个工具、炸了另一个流程的情况。
我个人实际用下来的体会是:搭 Harness Agent 开发环境,真正难的不是框架本身,而是你有没有一套约束不确定性的工程方法。把 LLM 的输出圈在一个小盒子里,其余全部用确定性代码兜底,这就是可靠性最实在的秘诀。用 Android 登录模块当第一个试点,看起来只是选了个简单的业务,但它逼着你把状态、校验、权限、日志、回归全部想清楚——而这些恰恰是任何一个 Agent 项目走向生产环境都绕不开的关卡。先把这个关卡打通,后面接什么模块都会轻松很多。