大语言模型(LLM)的崛起让智能体(AI Agent)成为技术圈的热门话题。无论是自动化客服、个性化助手,还是复杂任务求解,智能体正逐步渗透到各个领域。而LangChain作为一款强大的LLM应用框架,凭借其模块化设计和丰富的工具链,成为开发者构建智能体的首选工具之一。

本文将带你从零开始,使用LangChain开发智能体,并分享代码和实现思路。


多智能体?

在上篇文章中,我们学习了怎么创建一个LangChain智能体。怎么让智能体使用工具。怎么流式输出智能体步骤结果。在本篇内容中,我们主要探索一下怎么使用多智能体协调工作。或组装智能体工作流。

开发环境准备

开发环境依旧是我们上一篇内容的开发环境。

多智能体设计思路

上篇文章中,我们知道了怎么创建一个带有工具的智能体。那么我们多智能体协调的思路是创建一个主智能体,其主要职责是进行意图识别、工作分工、任务调度等。然后我们封装几个工具。将各个子智能体封装成一个工具供主智能体使用。

主智能体实现

from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

# 加载环境变量
load_dotenv()

model = ChatOpenAI(
    model="deepseek-chat",
    base_url="https://api.deepseek.com",
    api_key=os.getenv("OPENAI_API_KEY", "")
)

agent = create_agent(
    model=model,
    system_prompt="你是一个资深的接口测试工程师,当用户发送给你接口文档是,首先使用工具解析接口信息。其次将解析的接口信息传给脚本生成工具生成测试脚本,包括正用例和反用例,并执行接口测试。最后将测试结果传给报告工具进行总结,生成测试报告。",
    tools=[analyze_api, generate_script, report_tool],
)

接口分析智能体

async def analyze_api(api_doc):
    """
    解析接口文档信息
    :return:
        dict: {
    	"domain": "https:www.baidu.com",
    	"url": "/login",
    	"method": "post",
    	"data_type": "json",
    	"headers": {
    		"Content-Type": "application/json"
    	},
    	"params": {
    		"user_name": "test",
    		"password": "123123123123"
    	}
    }
    """
    model_instance = ChatOpenAI(
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        api_key=os.getenv("OPENAI_API_KEY", "")
    )

    prompt = """
    你是接口文档分析工程师,你的职责是将用户的接口文档进行整理。返回固定的json格式数据,格式如下:
    {
    	"domain": "https:www.baidu.com",
    	"url": "/login",
    	"method": "post",
    	"data_type": "json",
    	"headers": {
    		"Content-Type": "application/json"
    	},
    	"params": {
    		"user_name": "test",
    		"password": "123123123123"
    	}
    }
    """

    analyze_api_agent = create_agent(
        model=model_instance,
        system_prompt=prompt,
    )

    # 使用异步流式调用
    async for chunk in analyze_api_agent.astream(
            {"messages": [{"role": "user", "content": api_doc}]}
    ):
        for step, data in chunk.items():
            print(f"step: {step}")
            print(f"content: {data['messages'][-1].content_blocks}")

脚本执行工具

import os
import mimetypes
from typing import Dict, Any
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# 配置会话池和重试策略
session = requests.Session()
retry_strategy = Retry(
    total=3,
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "POST"],
    backoff_factor=1
)
session.mount("https://", HTTPAdapter(max_retries=retry_strategy))
session.mount("http://", HTTPAdapter(max_retries=retry_strategy))


def get_mime_type(filename: str) -> str:
    """自动检测文件MIME类型"""
    mime_type, _ = mimetypes.guess_type(filename)
    return mime_type or 'application/octet-stream'


def safe_file_opener(file_path: str):
    """安全打开文件(上下文管理器)"""
    return open(file_path, 'rb')


async def api_test_tools(test_body: Dict[str, Any]) -> Dict[str, Any]:
    """
    智能接口测试执行器

    Args:
        test_body: 测试请求规范,包含:
            - data_type: 参数类型 (params|json|form-data|form-urlencoded)
            - URL: 接口路径
            - method: HTTP方法
            - params: 请求参数
            - headers: 请求头 (可选)
            - file: 上传文件路径 (可选)

    Returns:
        接口响应或错误信息
    """
    # 参数校验与预处理

    data_type = test_body.get('data_type', '').lower()
    required_keys = ['URL', 'method', 'params']

    if not all(key in test_body for key in required_keys):
        return {'code': '400', 'msg': '缺少必填参数'}

    if data_type not in ['params', 'json', 'form-data', 'form-urlencoded']:
        return {'code': '400', 'msg': f'不支持的参数类型: {data_type}'}

    # 构建完整请求参数
    request_spec = {
        'url': test_body.get('domain', '')+ test_body.get('URL', ''),
        'method': test_body['method'].lower(),
        'data': test_body['params'],
        'headers': test_body.get('headers', {}),
        'file': test_body.get('file')
    }

    try:
        # 根据数据类型选择执行策略
        strategy = {
            'params': _execute_params,
            'json': _execute_json,
            'form-data': _execute_formdata,
            'form-urlencoded': _execute_urlencoded
        }
        return strategy[data_type](request_spec)
    except Exception as e:
        return {'code': '500', 'msg': f'测试执行异常: {str(e)}'}


def _execute_params(spec: Dict[str, Any]) -> Dict[str, Any]:
    try:
        response = session.get(
            spec['url'],
            params=spec['data'],
            headers=spec['headers']
        )
        response.raise_for_status()
        return _handle_response(response)
    except requests.RequestException as e:
        return {'code': str(e.response.status_code) if e.response else '500',
                'msg': f'请求失败: {str(e)}'}


def _execute_json(spec: Dict[str, Any]) -> Dict[str, Any]:
    try:
        response = session.request(
            method=spec['method'],
            url=spec['url'],
            json=spec['data'],
            headers=spec['headers']
        )
        return _handle_response(response)
    except requests.RequestException as e:
        return {'code': str(e.response.status_code) if e.response else '500',
                'msg': f'请求失败: {str(e)}'}


def _execute_urlencoded(spec: Dict[str, Any]) -> Dict[str, Any]:
    try:
        response = session.request(
            method=spec['method'],
            url=spec['url'],
            data=spec['data'],
            headers=spec['headers']
        )
        return _handle_response(response)
    except requests.RequestException as e:
        return {'code': str(e.response.status_code) if e.response else '500',
                'msg': f'请求失败: {str(e)}'}


def _execute_formdata(spec: Dict[str, Any]) -> Dict[str, Any]:
    file_path = spec.get('file')
    if not file_path:
        return {'code': '400', 'msg': '文件上传模式需要指定file参数'}

    if not os.path.exists(file_path):
        return {'code': '404', 'msg': f'文件不存在: {file_path}'}

    try:
        with safe_file_opener(file_path) as f:
            files = {
                'file': (os.path.basename(file_path),
                         f,
                         get_mime_type(file_path))
            }
            response = session.request(
                method=spec['method'],
                url=spec['url'],
                data=spec['data'],
                files=files,
                headers=spec['headers']
            )
        return _handle_response(response)
    except requests.RequestException as e:
        return {'code': str(e.response.status_code) if e.response else '500',
                'msg': f'上传失败: {str(e)}'}
    except IOError as e:
        return {'code': '500', 'msg': f'文件读取失败: {str(e)}'}


def _handle_response(response: requests.Response) -> Dict[str, Any]:
    """统一处理响应"""
    try:
        # 尝试解析JSON响应
        return response.json()
    except ValueError:
        # 非JSON响应处理
        return {
            'status_code': response.status_code,
            'text': response.text,
            'headers': dict(response.headers)
        }

脚本生成智能体

async def generate_script(test_body):
    """
        根据接口信息生成测试脚本并执行

        Args:
            test_body: 接口测试相关信息

        Returns:
            list: 测试执行结果列表
        """
    model_instance = ChatOpenAI(
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        api_key=os.getenv("OPENAI_API_KEY", "")
    )

    prompt = """
    你是资深接口测试工程师,你的职责是将test_body的接口信息生成正反用例,并生成脚本,调用api_test_tools工具执行生成的脚本。将执行结果集合发送给生成报告智能体。
    保证test_body的格式为,没有的使用空字符串:
    {
    	"domain": "https:www.baidu.com",
    	"url": "/login",
    	"method": "post",
    	"data_type": "json",
    	"headers": {
    		"Content-Type": "application/json"
    	},
    	"params": {
    		"user_name": "test",
    		"password": "123123123123"
    	}
    }
    注意:接口失败不需要重试,只需要返回失败的结果给生成报告工具
    """

    analyze_api_agent = create_agent(
        model=model_instance,
        system_prompt=prompt,
        tools=[api_test_tools]
    )

    # 使用异步流式调用
    async for chunk in analyze_api_agent.astream(
            {"messages": [{"role": "user", "content": test_body}]}
    ):
        for step, data in chunk.items():
            print(f"step: {step}")
            print(f"content: {data['messages'][-1].content_blocks}")

生成报告智能体

async def report_tool(result_list):
    """
       生成测试报告

       Args:
           result_list: 测试结果列表

       Returns:
           str: 格式化的测试报告
       """
    model_instance = ChatOpenAI(
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        api_key=os.getenv("OPENAI_API_KEY", "")
    )

    prompt = """
    你的职责是总结测试结果。将测试结果以markdown的格式输出。
    """

    analyze_api_agent = create_agent(
        model=model_instance,
        system_prompt=prompt
    )

    # 使用异步流式调用
    async for chunk in analyze_api_agent.astream(
            {"messages": [{"role": "user", "content": result_list}]}
    ):
        for step, data in chunk.items():
            print(f"step: {step}")
            print(f"content: {data['messages'][-1].content_blocks}")

执行智能体

async def main():
    api_doc = """
    url
    http://www.baidu.com/s
    请求方式
    GET
    请求类型
    params
    wd=微信公众平台
    """

    # 开启异步流式响应
    async for chunk in agent.astream(
        {"messages": [{"role": "user", "content": api_doc}]}
    ):
        for step, data in chunk.items():
            print(f"step: {step}")
            print(f"content: {data['messages'][-1].content_blocks}")

# 运行异步函数
if __name__ == "__main__":
    asyncio.run(main())

执行结果

step: model
content: [{'type': 'text', 'text': '我来帮您进行接口测试。首先,让我解析您提供的接口文档信息。'}, {'type': 'tool_call', 'id': 'call_00_nTitIDfLnzDnfJvUdR6jtBAd', 'name': 'analyze_api', 'args': {'api_doc': '    url\n    http://www.baidu.com/s\n    请求方式\n    GET\n    请求类型\n    params\n    wd=微信公众平台'}}]
step: model
content: [{'type': 'text', 'text': '{\n    "domain": "http://www.baidu.com",\n    "url": "/s",\n    "method": "GET",\n    "data_type": "params",\n    "headers": {},\n    "params": {\n        "wd": "微信公众平台"\n    }\n}'}]
step: tools
content: [{'type': 'text', 'text': 'null'}]
step: model
content: [{'type': 'text', 'text': '让我重新整理一下接口文档信息,使用更清晰的格式:'}, {'type': 'tool_call', 'id': 'call_00_dAQJqF0dz4KLQPO3vc8fPfIz', 'name': 'analyze_api', 'args': {'api_doc': '接口文档:\nurl: http://www.baidu.com/s\n请求方式: GET\n请求类型: params\n参数: wd=微信公众平台'}}]
step: model
content: [{'type': 'text', 'text': '```json\n{\n    "domain": "http://www.baidu.com",\n    "url": "/s",\n    "method": "GET",\n    "data_type": "params",\n    "headers": {},\n    "params": {\n        "wd": "微信公众平台"\n    }\n}\n```'}]
step: tools
content: [{'type': 'text', 'text': 'null'}]
step: model
content: [{'type': 'text', 'text': '让我尝试使用更结构化的格式:'}, {'type': 'tool_call', 'id': 'call_00_Bty7Na715nZM4OAr7tLE4JMA', 'name': 'analyze_api', 'args': {'api_doc': '{\n  "url": "http://www.baidu.com/s",\n  "method": "GET",\n  "data_type": "params",\n  "params": {\n    "wd": "微信公众平台"\n  }\n}'}}]
step: model
content: [{'type': 'text', 'text': '```json\n{\n    "domain": "http://www.baidu.com",\n    "url": "/s",\n    "method": "GET",\n    "data_type": "params",\n    "headers": {\n        "Content-Type": "application/x-www-form-urlencoded"\n    },\n    "params": {\n        "wd": "微信公众平台"\n    }\n}\n```'}]
step: tools
content: [{'type': 'text', 'text': 'null'}]
step: model
content: [{'type': 'text', 'text': '看起来工具可能有一些问题。让我直接基于您提供的接口信息来生成测试脚本。根据您提供的信息,我了解到:\n\n1. 接口URL: http://www.baidu.com/s\n2. 请求方式: GET\n3. 请求类型: params (查询参数)\n4. 参数: wd=微信公众平台\n\n现在让我基于这些信息生成测试脚本:'}, {'type': 'tool_call', 'id': 'call_00_Ka00H027BPhVKgVS1fDL6iHW', 'name': 'generate_script', 'args': {'test_body': {'domain': 'http://www.baidu.com', 'url': '/s', 'method': 'GET', 'data_type': 'params', 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}, 'params': {'wd': '微信公众平台'}}}}]
step: tools
content: [{'type': 'text', 'text': "Error invoking tool 'generate_script' with kwargs {'test_body': {'domain': 'http://www.baidu.com', 'url': '/s', 'method': 'GET', 'data_type': 'params', 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}, 'params': {'wd': '微信公众平台'}}} with error:\n content.str: Input should be a valid string\ncontent.list[union[str,dict[any,any]]]: Input should be a valid list\n Please fix the error and try again."}]
step: model
content: [{'type': 'text', 'text': '看起来参数格式有问题。让我重新尝试:'}, {'type': 'tool_call', 'id': 'call_00_fXNpGHwNfsqYa7SSikT2sksn', 'name': 'generate_script', 'args': {'test_body': '{\n  "domain": "http://www.baidu.com",\n  "url": "/s",\n  "method": "GET",\n  "data_type": "params",\n  "headers": {\n    "Content-Type": "application/x-www-form-urlencoded"\n  },\n  "params": {\n    "wd": "微信公众平台"\n  }\n}'}}]
step: model
content: [{'type': 'text', 'text': '我将为您分析这个接口并生成正反用例,然后执行测试。首先让我分析一下接口信息:\n\n**接口分析:**\n- 域名:http://www.baidu.com\n- 路径:/s\n- 方法:GET\n- 参数类型:params\n- 参数:wd="微信公众平台"\n\n现在我将生成正反用例并执行测试:'}, {'type': 'tool_call', 'id': 'call_00_zmfBC7V0rf0f7Fw1Evfyoz8b', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.baidu.com/s', 'method': 'GET', 'params': {'wd': '微信公众平台'}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"status_code": 200, "text": "<!DOCTYPE html>\\n<html la...}]
step: model
content: [{'type': 'text', 'text': '现在让我测试一些边界情况和异常情况:'}, {'type': 'tool_call', 'id': 'call_00_3DWzlVEpCyURwxtfyiUAZ0SZ', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.baidu.com/s', 'method': 'GET', 'params': {'wd': ''}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"status_code": 200, "text": "<!DOCTYPE html>\\n<html>\\n...'}]
step: model
content: [{'type': 'text', 'text': '现在测试一个特殊字符的搜索:'}, {'type': 'tool_call', 'id': 'call_00_Q1guVxt8CM7l9zj0TzFOMmWv', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.baidu.com/s', 'method': 'GET', 'params': {'wd': 'test@#$%^&*()_+'}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"status_code": 200, "text": "<!DOCTYPE html>\\n<htm...'}]
step: model
content: [{'type': 'text', 'text': '现在测试一个超长字符串:'}, {'type': 'tool_call', 'id': 'call_00_ZybY6PRr8tamLYsJfi2fbVcE', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.baidu.com/s', 'method': 'GET', 'params': {'wd': '这是一个非常长的搜索关键词,用于测试百度搜索接口对长字符串的处理能力,看看是否会截断或者返回错误信息,这个字符串应该超过100个字符以上,以确保测试的完整性。'}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"status_code": 200, "text": "<!DOCTYPE h...'}]
step: model
content: [{'type': 'text', 'text': '现在测试一个不存在的域名,看看错误处理:'}, {'type': 'tool_call', 'id': 'call_00_kcEjNXLRGtzSoYQi1umbY1EC', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.nonexistentdomain12345.com/s', 'method': 'GET', 'params': {'wd': 'test'}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"code": "500", "msg": "请求失败: HTTPConnectionPool(host=\'www.nonexistentdomain12345.com\', port=80): Max retries exceeded with url: /s?wd=test (Caused by NameResolutionError(\\"<urllib3.connection.HTTPConnection object at 0x7588fc35c950>: Failed to resolve \'www.nonexistentdomain12345.com\' ([Errno -2] Name or service not known)\\"))"}'}]
step: model
content: [{'type': 'text', 'text': '现在测试一个错误的URL路径:'}, {'type': 'tool_call', 'id': 'call_00_Sa7oiTDSYji7F2Kh6FCM5G4V', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.baidu.com/nonexistent', 'method': 'GET', 'params': {'wd': 'test'}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"code": "500", "msg": "请求失败: 404 Client Error: Not Found for url: http://www.baidu.com/nonexistent?wd=test"}'}]
step: model
content: [{'type': 'text', 'text': '现在测试一个错误的HTTP方法:'}, {'type': 'tool_call', 'id': 'call_00_x3YJiHgc6QRzqP0otpwKcSzk', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.baidu.com/s', 'method': 'POST', 'params': {'wd': 'test'}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"status_code": 200, "text": "<!DOCTYPE htm...'}]
step: model
content: [{'type': 'text', 'text': '现在测试一个正常的英文搜索:'}, {'type': 'tool_call', 'id': 'call_00_WrTjQDOFzHsijNxp0MfzPSe7', 'name': 'api_test_tools', 'args': {'test_body': {'data_type': 'params', 'URL': 'http://www.baidu.com/s', 'method': 'GET', 'params': {'wd': 'python programming'}, 'headers': {'Content-Type': 'application/x-www-form-urlencoded'}}}}]
step: tools
content: [{'type': 'text', 'text': '{"status_code": 200, "text": "<!DOCTYPE html...'}]
step: model
content: [{'type': 'text', 'text': '## 测试执行完成\n\n我已经为百度搜索接口 `/s` 执行了全面的正反用例测试。以下是测试结果的总结:\n\n### **测试用例设计:**\n\n**正向用例:**\n1. **正常中文搜索** - 搜索"微信公众平台"(状态码:200,返回安全验证页面)\n2. **空搜索词** - 搜索空字符串(状态码:200,返回百度首页)\n3. **英文搜索** - 搜索"python programming"(状态码:200,返回安全验证页面)\n4. **特殊字符搜索** - 搜索"test@#$%^&*()_+"(状态码:200,返回安全验证页面)\n5. **超长字符串搜索** - 搜索超过100字符的长文本(状态码:200,返回安全验证页面)\n\n**反向用例:**\n1. **错误HTTP方法** - 使用POST方法(状态码:200,返回安全验证页面)\n2. **错误URL路径** - 访问不存在的路径(状态码:404,返回404错误)\n3. **不存在的域名** - 访问不存在的域名(状态码:500,域名解析失败)\n\n### **测试结果分析:**\n\n**主要发现:**\n1. **安全验证机制**:百度搜索接口对大多数请求都返回了安全验证页面,而不是实际的搜索结果页面\n2. **接口稳定性**:所有有效的请求都返回了200状态码,说明接口本身是稳定的\n3. **错误处理**:\n   - 不存在的域名:返回500错误,域名解析失败\n   - 不存在的路径:返回404错误\n   - 其他异常情况:都返回了安全验证页面\n\n**接口行为特点:**\n- 百度搜索接口似乎有安全验证机制,对于非正常访问会返回安全验证页面\n- 即使使用错误的HTTP方法(如POST),接口也返回200状态码\n- 接口对参数长度和特殊字符都有较好的容错性\n\n**建议:**\n1. 对于生产环境测试,可能需要处理安全验证机制\n2. 接口的异常处理机制比较完善\n3. 可以考虑测试更多边界情况,如超长参数、特殊编码等\n\n所有测试结果已收集完成,可以发送给生成报告智能体进行详细分析和报告生成。'}]

总结

现在我们掌握了使用langchain创建智能体工作流或多智能体协同了,但是因为文章篇幅限制,提示词写的比较少,要想要更好的效果,还需要继续调试提示词。本篇文章为了展示效果,省略了提示词,可以根据自身情况,继续优化提示词。后面会继续更新langchain的使用方法。欢迎关注,防止迷路。

更多推荐