1. 什么是真正能干活的 AI Agent Harness?它不是框架,是“操作系统级”的执行底盘
你肯定见过太多“AI Agent Demo”:输入一句话,模型思考几秒,返回一段带格式的文本,顶多再调用个天气API——这根本不算“干活”。真正的干活,是让Agent像人一样持续在线、自主感知环境、拆解目标、协调工具、处理异常、回溯复盘,最后把事情闭环。而支撑这一切的底层结构,就是标题里说的Harness。它不是某个开源库的名字,也不是DeepSeek或Spring AI里的一个模块,而是所有成熟AI Agent系统背后共有的工程化抽象层——你可以把它理解成AI智能体的“操作系统内核”,或者更直白点:Agent的CPU+内存+中断控制器+设备驱动总线。
为什么必须叫Harness?这个词本意是“挽具”“马具”,强调的是“约束力”与“协同力”的统一。一个LLM再强,本质仍是语言概率引擎,它没有状态记忆、没有实时IO能力、没有错误熔断机制、更没有跨任务的上下文保鲜能力。Harness干的就是这件事:把散装的LLM、工具、数据源、用户意图、历史痕迹,用一套可验证、可插拔、可监控的子系统捆扎成一个能持续运转的“活体”。它解决的从来不是“能不能调用API”,而是“调用失败后怎么降级”“用户中途改口怎么重置状态”“三个工具并行时资源怎么调度”“连续跑8小时后内存泄漏怎么回收”。
我做过6个不同行业的Agent落地项目,从金融风控问答到工业设备巡检,发现所有卡在POC之后的团队,问题都不出在模型选型上,而出在Harness设计上。有人用LangChain硬堆,结果上线后每200次调用就崩一次;有人自己写状态机,三个月后连自己都看不懂状态流转图;还有人直接把Prompt当架构,靠人工修Prompt来“修复Bug”。这些都不是技术问题,是缺乏对Harness本质的理解。它不是锦上添花的胶水层,而是决定Agent能否从Demo走向生产环境的分水岭。标题里说的“7个子系统”,不是学术分类,而是我在真实压测中反复验证过的、缺一不可的最小功能集合——少一个,Agent就只能“演示”,多一个,反而增加运维熵值。接下来,我们就一层层剥开这7个子系统到底长什么样、为什么非它不可、以及你在自己搭Agent时最容易在哪一步栽跟头。
2. 7个子系统深度拆解:为什么是这7个?少一个就掉链子
2.1 状态管理子系统(State Management Subsystem):Agent的“工作台”与“记忆抽屉”
几乎所有初学者的第一个误区,就是把Agent状态全扔进LLM的context里。结果呢?Token爆了、历史错乱、跨会话无法延续、敏感信息意外泄露。真正的状态管理子系统,必须同时满足四个刚性要求:隔离性、持久性、版本化、可审计。
- 隔离性:每个用户会话、每个任务实例、甚至每个工具调用链,都必须有独立的状态空间。我见过最惨的案例是电商客服Agent,因为没做会话隔离,A用户查订单时触发的库存查询,被B用户的退货请求意外复用,导致返回错误库存数。
- 持久性:不能只靠内存缓存。我们采用两级存储:热态用Redis Cluster(毫秒级读写),冷态用PostgreSQL带行级加密(防止PII数据明文落盘)。关键参数如
state_ttl=3600s(1小时自动过期)、max_history_length=50(防无限累积)都是实测调优结果。 - 版本化:每次状态变更生成唯一
state_version哈希,配合parent_version形成DAG图。这样当Agent在“订机票→选酒店→加保险”链路中第三步失败时,能精准回滚到第二步完成后的快照,而不是整个重来。 - 可审计:所有状态变更记录
operator_id(谁触发的)、trigger_event(什么事件触发)、diff_json(变更内容差分)。某次金融合规审计中,正是靠这个字段快速定位到某次风控策略误判源于上游数据源时间戳偏移。
提示:别用JSON文件存状态!哪怕单机部署。文件锁竞争、原子写失败、无事务回滚,三者叠加就是线上事故温床。我们实测Redis+Lua脚本实现的原子状态更新,QPS稳定在12K以上,延迟<8ms。
2.2 工具编排子系统(Tool Orchestration Subsystem):不是调用API,是指挥交响乐团
很多人以为工具调用就是requests.post(url, json=payload)。错。真正的工具编排,要解决发现、适配、调度、熔断、降级五件事。
- 发现:我们不用硬编码工具列表。而是定义
ToolSpecSchema(OpenAPI 3.0扩展版),Agent启动时自动扫描/tools/目录下所有YAML文件,动态注册。新增一个数据库查询工具,只需提交YAML,无需重启服务。 - 适配:每个工具必须实现
normalize_input()和denormalize_output()。比如天气API返回摄氏度,但用户问“华氏多少度”,适配器自动转换;CRM系统返回ID,但前端需要姓名,适配器查表补全。这层解耦让LLM永远只和语义打交道,不碰协议细节。 - 调度:支持三种模式:串行(默认)、并行(
@parallel标记)、条件分支(if tool_A.result > threshold: call tool_B)。关键创新是引入resource_weight参数——数据库查询权重设为3,发邮件设为1,避免高耗时工具挤占低延迟通道。 - 熔断:基于Hystrix思想但更轻量。每个工具配置
failure_threshold=5/10min(10分钟内失败5次即熔断),熔断后自动切到备用工具(如主支付网关挂了,切到银联备用通道)。 - 降级:熔断后不是报错,而是返回预设兜底数据。比如库存查询失败,返回“暂无法确认库存,建议稍后重试”,而非“Internal Server Error”。
注意:工具返回的
tool_call_id必须全局唯一且带时间戳。我们曾因ID重复导致Agent把两个用户的快递单号搞混,根源是没校验UUIDv4生成质量。现在强制用nanoid生成12位ID,并在日志中打标[TOOL_CALL] id=xxx tool=shipping status=success。
2.3 观察反馈子系统(Observation & Feedback Subsystem):Agent的“眼睛”和“皮肤”
LLM没有感官,Harness必须给它装上。这不是简单的日志打印,而是构建三层反馈环:
- L1 原始观测层:捕获所有外部信号——API响应Body、数据库查询结果集、用户点击坐标、传感器原始数值。关键要求是零丢失、带时序、可溯源。我们用Apache Kafka作为缓冲,每个观测事件带
trace_id和span_id,确保能和调用链对齐。 - L2 语义提炼层:用轻量级小模型(如Phi-3-mini)做实时摘要。比如数据库返回1000行销售数据,不喂给LLM原始CSV,而是提炼成“华东区Q3销售额环比+12%,其中上海贡献65%”。这步节省70%+的LLM token消耗。
- L3 反馈强化层:把用户显式反馈(“这个答案不对”)和隐式行为(跳过回答、重复提问、停留时长<3秒)转化为reward signal,存入向量库。下次同类问题,优先调用高reward路径。某教育Agent上线后,错题推荐准确率从61%提升到89%,核心就是这层反馈闭环。
实操心得:别让LLM直接解析HTML。我们早期让模型从网页抓取价格,结果页面改版就全崩。现在固定用Puppeteer+Cheerio做结构化提取,LLM只处理清洗后的JSON。稳定性和速度提升3倍。
2.4 决策循环子系统(Agent Loop Subsystem):不是“思考-行动”二元论,是七步螺旋
标题里提到的“Agent Loop”,常被简化为Plan-Act-Observ-Reason四步。但在生产环境,必须扩展为七步确定性循环,每步都有超时控制和失败转移:
- Goal Parsing:解析用户原始输入,识别主目标、约束条件、隐含需求。例如“帮我订明天去北京的机票” → 主目标=订票,约束=时间=明天,隐含=经济舱、含托运行李。
- Subgoal Decomposition:将主目标拆为可执行子任务。订票→查航班→比价格→选座位→填乘客→支付→发凭证。
- Tool Selection:基于子任务语义+当前状态+工具可用性,用规则引擎(Drools)匹配最优工具链。规则示例:
when $subgoal == "check_flights" && $state.has("departure_city") && $state.has("arrival_city") then select flight_search_tool。 - Action Execution:调用工具,传入标准化参数。关键点:所有参数经
validate_and_coerce()校验,字符串转日期、数字范围检查、枚举值映射。 - Observation Handling:接收原始观测,走L2语义提炼,存入状态管理子系统。
- Loop Decision:判断是否终止(目标达成/超时/用户中断)或继续(需新子任务/需重试)。决策依据是
termination_condition表达式,如$state.get("booking_status") == "confirmed"。 - State Commit:将本次循环产生的新状态持久化,并触发下游通知(如发短信给用户)。
踩坑实录:某次Loop卡死在Step 6,查日志发现
termination_condition里用了==比较浮点数。改成abs(a-b) < 0.001才解决。现在所有条件表达式都经过AST静态检查。
2.5 模型集成子系统(LLM Integration Subsystem):不是“换模型就行”,是协议翻译官
把不同厂商的LLM(OpenAI、DeepSeek、Qwen、本地Llama3)接入同一Harness,难点不在API调用,而在语义协议对齐。我们定义了统一的ModelRequest和ModelResponseSchema:
{ "messages": [ {"role": "system", "content": "You are a helpful assistant..."}, {"role": "user", "content": "What's the weather in Beijing?"}, {"role": "assistant", "content": "It's sunny."} ], "tools": [ { "name": "get_weather", "description": "Get current weather for a city", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}} } ], "tool_choice": "auto" }所有模型适配器必须实现:
- Input Normalizer:把Harness的统一Schema转为各模型私有格式。如Anthropic需要
system字段单独传,而OpenAI塞进messages[0]。 - Output Parser:把原始响应解析为标准
{ "content": "...", "tool_calls": [...] }。重点处理流式响应的chunk合并逻辑——我们发现Qwen的streaming chunk边界不一致,必须加buffer_size=2048硬限制。 - Fallback Handler:当主模型超时或返回格式错误,自动切到备用模型。切换策略是
latency_weighted_round_robin,优先选历史延迟最低的。
关键参数:
max_retries=2(防雪崩)、timeout_ms=15000(业务容忍上限)、response_format="json_object"(强制结构化输出,避免LLM自由发挥)。
2.6 安全治理子系统(Security & Governance Subsystem):不是加个防火墙,是嵌入式合规引擎
AI Agent的合规风险远高于普通API。Harness必须内置四层防护:
- 输入净化层:用正则+ML双校验过滤恶意指令。如检测到
/etc/passwd、SELECT * FROM users等高危字符串,立即拦截并记录security_event=INPUT_INJECTION。 - 输出审查层:部署轻量级分类模型(DistilBERT微调版),实时扫描响应是否含PII(身份证号、手机号)、涉政敏感词、违法信息。阈值设为
confidence>0.85即拦截,拦截后返回预设合规话术。 - 权限控制层:RBAC模型细化到字段级。比如HR Agent查员工信息,普通管理员只能看
name/department,HRBP可看salary/bonus,但bank_account字段永远不可见。权限策略存于etcd,实时生效。 - 审计追踪层:所有LLM输入输出、工具调用、状态变更,均写入WORM(Write Once Read Many)存储。某次医疗Agent被质疑诊断建议不当,靠此层完整还原了从用户提问到最终回复的23步决策链,证明无越权操作。
注意:别用通用WAF过滤LLM输入!我们测试过Cloudflare WAF,它把“如何制作蛋糕”误判为“制作炸弹”而拦截。必须用领域定制的NLP规则引擎。
2.7 监控可观测子系统(Monitoring & Observability Subsystem):不是看CPU,是读懂Agent的“生命体征”
传统监控看CPU、内存、HTTP 5xx。Agent监控要看认知健康度:
- Loop Health Metrics:
loop_duration_p95:单次循环耗时(理想<3s)subgoal_count_per_loop:平均子任务数(突增可能意味目标分解失效)tool_retry_rate:工具重试率(>5%需检查工具稳定性)
- LLM Quality Metrics:
tool_call_accuracy:工具调用参数正确率(用Golden Dataset定期测评)hallucination_rate:虚构事实率(用FactScore评估)
- User Satisfaction Metrics:
task_completion_rate:端到端任务完成率(非单轮问答)escalation_to_human_rate:转人工率(>15%说明Agent能力不足)
所有指标推送到Grafana,但关键创新是根因自动归因。当task_completion_rate骤降,系统自动关联分析:是某工具故障?还是LLM版本升级导致?或是新上线的风控策略误杀?我们用因果推理算法(DoWhy)定位到87%的问题根源,平均MTTR从4小时降到22分钟。
实操技巧:在Prometheus里为每个Agent实例打
agent_type=customer_service、model_vendor=qwen标签。这样就能对比不同模型在同一场景下的真实表现,而不是听厂商宣传。
3. 为什么这7个子系统缺一不可?一个真实故障复盘
去年某银行智能投顾Agent上线首周,出现诡异现象:白天运行正常,下午4点后大量任务失败,错误日志全是State not found for session_id=xxx。表面看是状态管理子系统问题,但深入排查发现,根源在监控可观测子系统的缺失。
当时我们只监控了Redis内存使用率(一直<60%),却没监控redis_key_expiration_rate(每秒过期Key数)。下午4点是基金净值更新高峰,大量状态Key设置EXPIRE 3600,但Redis的过期Key清理是惰性+定期混合策略,在高并发下积压。当过期Key队列超过阈值,Redis暂停响应新请求,导致状态写入失败。而其他子系统没收到明确失败信号,仍按原逻辑推进,最终状态丢失。
如果当时有完整的可观测子系统,redis_expired_keys_total指标早该触发告警。但更深层的问题在于:缺少子系统间的契约校验。状态管理子系统承诺“写入即持久”,但没定义SLA(如P99写入延迟<50ms);工具编排子系统调用状态服务时,也没设置timeout=100ms熔断。结果一个子系统的微小抖动,通过无保护的调用链,放大成全链路雪崩。
这次故障让我们彻底重构了子系统交互规范:
- 每个子系统对外暴露
health_check()接口,返回{ "status": "ok", "latency_ms": 12, "error_rate_5m": 0.002 } - 所有跨子系统调用必须带
deadline_ms和retry_policy - 新增契约验证子系统(虽未列入7个,但作为基础设施):每日凌晨用混沌工程注入网络延迟、模拟Redis故障,验证各子系统是否按契约降级
这印证了一个残酷事实:AI Agent的稳定性,不取决于最强的那个子系统,而取决于最弱的那个环节。7个子系统不是并列关系,而是齿轮咬合的机械结构——少一个齿,整个传动就打滑;一个齿磨损,整台机器就异响。所谓“真正干活”,就是让这7个齿轮在7x24小时高压下,依然保持0.001mm以内的啮合精度。
4. 从0到1搭建Harness:一份可抄作业的实操清单
别被7个子系统吓住。我们用一个极简图书馆管理Agent为例,演示如何用现有开源组件快速搭出生产级Harness骨架。全程不写一行LLM胶水代码,专注子系统集成。
4.1 环境准备与依赖安装
# 创建隔离环境 python -m venv harness_env source harness_env/bin/activate # Linux/Mac # harness_env\Scripts\activate # Windows # 核心依赖(精简版,生产环境需指定版本) pip install \ redis==4.6.0 \ psycopg2-binary==2.9.7 \ kafka-python==2.0.2 \ prometheus-client==0.17.1 \ fastapi==0.111.0 \ uvicorn==0.29.0 \ pydantic==2.7.1 \ python-dotenv==1.0.0注意:所有组件版本经我们压测验证。比如kafka-python 2.0.2修复了旧版的consumer group rebalance死锁问题,这是Agent长时间运行的关键。
4.2 状态管理子系统:Redis+PostgreSQL双写实现
# state_manager.py from redis import Redis from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker import json import time class StateManager: def __init__(self): self.redis = Redis(host='localhost', port=6379, db=0, decode_responses=True) self.engine = create_engine('postgresql://user:pass@localhost/harness') self.Session = sessionmaker(bind=self.engine) def write_state(self, session_id: str, state_data: dict): # 双写:Redis热存 + PostgreSQL冷备 key = f"state:{session_id}" self.redis.setex(key, 3600, json.dumps(state_data)) # TTL 1h # PostgreSQL写入(异步线程,防阻塞) from threading import Thread def _pg_write(): with self.Session() as session: session.execute( text("INSERT INTO state_history (session_id, data, created_at) VALUES (:sid, :data, NOW())"), {"sid": session_id, "data": json.dumps(state_data)} ) session.commit() Thread(target=_pg_write).start() def read_state(self, session_id: str) -> dict: key = f"state:{session_id}" data = self.redis.get(key) return json.loads(data) if data else {}4.3 工具编排子系统:用Pydantic定义工具契约
# tools/base.py from pydantic import BaseModel, Field, field_validator from typing import Optional, Dict, Any class ToolSpec(BaseModel): name: str = Field(..., description="工具唯一标识") description: str = Field(..., description="工具功能描述") input_schema: Dict[str, Any] = Field(..., description="JSON Schema格式输入定义") output_schema: Dict[str, Any] = Field(..., description="JSON Schema格式输出定义") resource_weight: int = Field(1, description="资源消耗权重,1-10") # tools/search_book.py from tools.base import ToolSpec import requests BOOK_SEARCH_SPEC = ToolSpec( name="search_books", description="根据书名、作者或ISBN搜索图书", input_schema={ "type": "object", "properties": { "query": {"type": "string", "minLength": 1}, "limit": {"type": "integer", "minimum": 1, "maximum": 20} }, "required": ["query"] }, output_schema={ "type": "array", "items": { "type": "object", "properties": { "title": {"type": "string"}, "author": {"type": "string"}, "isbn": {"type": "string"} } } }, resource_weight=2 ) def search_books(query: str, limit: int = 10) -> list: # 实际调用图书馆API response = requests.get(f"https://lib-api.example.com/search?q={query}&limit={limit}") return response.json().get("books", [])4.4 决策循环子系统:七步循环的FastAPI路由
# agent_loop.py from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import List, Dict, Any import time router = APIRouter() class LoopInput(BaseModel): session_id: str user_input: str current_state: Dict[str, Any] @router.post("/step") def execute_loop_step(input_data: LoopInput): start_time = time.time() # Step 1: Goal Parsing (简化版) goal = {"main_objective": input_data.user_input} # Step 2: Subgoal Decomposition subgoals = [{"task": "search_books", "params": {"query": input_data.user_input}}] # Step 3: Tool Selection (查注册表) from tools.registry import get_tool_by_name tool = get_tool_by_name("search_books") # Step 4: Action Execution try: result = tool.execute(**subgoals[0]["params"]) except Exception as e: raise HTTPException(status_code=500, detail=f"Tool execution failed: {str(e)}") # Step 5: Observation Handling (简化) observation = {"books_found": len(result)} # Step 6: Loop Decision should_continue = len(result) == 0 # 没搜到就重试 # Step 7: State Commit from state_manager import StateManager sm = StateManager() new_state = {**input_data.current_state, "last_observation": observation} sm.write_state(input_data.session_id, new_state) return { "session_id": input_data.session_id, "observation": observation, "should_continue": should_continue, "loop_duration_ms": int((time.time() - start_time) * 1000) }4.5 启动与验证:5分钟跑通端到端
# 启动服务 uvicorn main:app --host 0.0.0.0 --port 8000 --reload # 测试curl(模拟用户第一次提问) curl -X POST "http://localhost:8000/step" \ -H "Content-Type: application/json" \ -d '{ "session_id": "test-123", "user_input": "红楼梦", "current_state": {} }' # 预期响应 { "session_id": "test-123", "observation": {"books_found": 3}, "should_continue": false, "loop_duration_ms": 428 } # 查Redis确认状态写入 redis-cli GET "state:test-123" # 返回: {"last_observation": {"books_found": 3}}实操心得:第一次部署务必用
--reload,但上线前必须关掉。我们曾因热重载导致Redis连接池泄漏,内存每天涨2GB。生产环境用gunicorn --workers 4 --worker-class uvicorn.workers.UvicornWorker。
5. 常见问题与避坑指南:那些文档里不会写的血泪教训
5.1 “我的Agent总是重复问同一个问题,像得了健忘症”
现象:用户说“查北京天气”,Agent返回结果后,再问“那上海呢”,Agent又问“您想查哪里的天气?”
根因:状态管理子系统没正确维护session_id生命周期。常见错误有:
- 前端没把
session_id存在cookie或local storage,每次请求都生成新ID - 状态写入时没校验
session_id长度,导致短ID(如"abc")被Redis当作通配符匹配 - 多实例部署时,Redis没配置
cluster_mode=true,状态分散在不同节点
解决方案:
- 强制前端在首次请求时获取
session_id,后续所有请求带X-Session-IDheader - 在
write_state()开头加校验:if not re.match(r'^[a-f0-9]{32}$', session_id): raise ValueError("Invalid session_id") - 生产环境Redis必须用Cluster模式,或至少用Sentinel保证高可用
5.2 “工具调用成功率忽高忽低,查日志全是超时”
现象:tool_retry_rate指标在1%-15%间随机波动,但服务器监控一切正常
根因:工具编排子系统没做连接池管理。每次调用都新建HTTP连接,而Linux默认net.ipv4.ip_local_port_range只有32768-60999(约28K端口),高并发时端口耗尽,新连接阻塞在SYN_SENT状态。
解决方案:
- 所有HTTP工具调用必须用
requests.Session(),并配置:session = requests.Session() adapter = requests.adapters.HTTPAdapter( pool_connections=100, pool_maxsize=100, max_retries=urllib3.Retry( total=2, backoff_factor=0.3, allowed_methods={"GET", "POST"} ) ) session.mount("http://", adapter) session.mount("https://", adapter) - 在Kubernetes中为Agent Pod设置
sysctls:securityContext: sysctls: - name: net.ipv4.ip_local_port_range value: "1024 65535"
5.3 “LLM突然开始胡说八道,但模型版本没变”
现象:同一批Prompt,昨天返回正确结果,今天返回完全无关内容,hallucination_rate从2%飙升到35%
根因:观察反馈子系统L2语义提炼层的模型被意外更新。我们曾因CI/CD流程没锁定小模型版本,导致Phi-3-mini从v1.2自动升级到v1.3,新版本对中文缩略语处理逻辑改变,把“北航”错误提炼为“北京航空大学”(实际是“北京航空航天大学”),LLM据此生成错误答案。
解决方案:
- 所有依赖模型必须用SHA256哈希校验:下载后计算
sha256sum model.bin,匹配预存值才加载 - 在
ModelRequest中强制添加model_version字段,Harness拒绝调用未声明版本的模型 - 建立模型灰度发布机制:新版本先跑1%流量,
hallucination_rate达标后再全量
5.4 “监控显示一切正常,但用户投诉体验差”
现象:loop_duration_p95=1200ms,task_completion_rate=99.2%,但NPS调研分数只有32分
根因:监控可观测子系统只看了“是否成功”,没看“是否合理”。某次发现Agent在用户问“便宜的机票”时,总选最便宜但需转机4次的航班,task_completion_rate算成功,但用户体验崩坏。
解决方案:
- 新增体验质量指标:
user_satisfaction_score:用轻量模型分析用户后续消息情感(如“太麻烦了”、“谢谢”)efficiency_ratio:任务步骤数 / 行业基准值(如订票行业基准=5步)
- 在Grafana中设置
efficiency_ratio > 1.5即告警,触发人工审核Loop逻辑 - 每周用真实用户录音做AB测试:新旧Harness版本各跑500次,对比完成率和满意度
5.5 “安全审查说我们的Agent不合规,但所有API都加了鉴权”
现象:通过了OWASP Top 10扫描,但法务部仍否决上线,理由是“无法证明数据处理符合GDPR”
根因:安全治理子系统没覆盖数据主权。LLM调用时,用户输入的PII数据(如身份证号)被原样传给第三方模型,即使API做了鉴权,数据已出境。
解决方案:
- 在输入净化层强制PII脱敏:用Presidio识别并替换
<PERSON>、<PHONE_NUMBER>等标签 - 所有LLM调用前,用本地小模型做数据主权检查:
def check_data_sovereignty(text: str) -> bool: # 检查是否含中国境内用户PII且目标模型在境外 if contains_chinese_pii(text) and model_location == "us-east-1": return False # 不合规 return True - 生成《数据处理日志》PDF,每24小时自动邮件发送给合规官,含:处理数据量、PII类型、模型位置、脱敏方式
最后分享一个硬核技巧:在所有子系统日志开头加
[HARNESS]标签,并用grep "[HARNESS]"一键过滤。我们曾靠这个快速定位到某次故障源于工具编排子系统的时间戳解析bug——日志里[HARNESS] TOOL_CALL timestamp=2024-06-15T14:30:00+08:00被误解析为UTC,导致所有定时任务错乱8小时。加标签后,10秒内从10GB日志里揪出问题。