☰
用DeepSeek-Harness搭建Agent开发环境:以Android登录模块为例
2026/9/29 10:41:16 网站建设 项目流程

最近在折腾 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 项目走向生产环境都绕不开的关卡。先把这个关卡打通,后面接什么模块都会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询