跳到主要内容

AI 聊天助手

概述

Evatar 的 AI 聊天助手是一个基于 LLM 的 Agent 系统,具备工具调用 (Tool Calling) 能力。它能够搜索用户的截图知识库、获取最近截图、联网搜索互联网,以及利用记忆系统为用户提供个性化的回答。

Agent 循环

可用工具

Agent 配备 4 个工具,用于扩展其知识获取能力:

search_knowledge — 单关键词搜索

搜索用户截图知识库,使用单个核心关键词:

{
"type": "function",
"function": {
"name": "search_knowledge",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词,用空格分隔多个词"}
},
"required": ["query"]
}
}
}

搜索策略:如果第一次没搜到,换同义词重试(如 "股票" -> "股价" -> "行情" -> "金融")。

search_multi — 多关键词同时搜索

一次传入多个相关关键词,自动合并去重返回:

{
"type": "function",
"function": {
"name": "search_multi",
"parameters": {
"type": "object",
"properties": {
"queries": {
"type": "array",
"items": {"type": "string"},
"description": "搜索关键词列表"
}
},
"required": ["queries"]
}
}
}

适合用户问题涉及多个主题时使用,例如:

  • "出行" -> ["火车", "高铁", "导航", "12306", "机票"]
  • "财务" -> ["支付", "捐赠", "转账", "银行", "红包"]

每个关键词最多搜索 5 条结果,最终合并去重返回最多 12 条。

get_recent — 获取最近截图

返回最近 N 条截图分析结果,无需搜索词:

{
"type": "function",
"function": {
"name": "get_recent",
"parameters": {
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "返回数量,默认10,最大20"}
}
}
}
}

适合 "我最近截了什么"、"帮我整理最近的内容" 等宽泛问题。

web_search — 搜索互联网

搜索互联网获取实时信息:

{
"type": "function",
"function": {
"name": "web_search",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索查询"}
},
"required": ["query"]
}
}
}

RAG 检索实现

FTS5 全文搜索

首选使用 SQLite FTS5 全文搜索引擎,对截图分析结果建立索引:

# services/rag.py
CREATE VIRTUAL TABLE analysis_fts USING fts5(
summary, app_name, content_category, intent, entities,
content='analyses', content_rowid='id'
)

索引包含 5 个字段:摘要、应用名、内容分类、意图和实体。搜索时使用 FTS5 MATCH 语法:

rows = db.execute(text("""
SELECT a.id, a.summary, a.app_name, a.content_category, a.intent,
a.entities, p.filename, p.original_timestamp
FROM analysis_fts
JOIN analyses a ON a.id = analysis_fts.rowid
JOIN photos p ON p.id = a.photo_id
WHERE analysis_fts MATCH :query
ORDER BY rank
LIMIT :limit
"""), {"query": fts_query, "limit": limit})

查询清洗

FTS5 查询会经过清洗,移除特殊字符防止语法错误:

_FTS_SPECIAL = re.compile(r'[^\w\s一-鿿]')

def _sanitize_fts_query(query: str) -> str:
tokens = query.split()
clean = []
for t in tokens[:10]:
t = _FTS_SPECIAL.sub("", t)
if t:
clean.append(t)
return " OR ".join(clean)

关键词回退搜索

当 FTS5 不可用或索引过期时,回退到 LIKE 关键词搜索:

def _keyword_search(db, query, limit):
keywords = query.split()[:5]
conditions = []
for i, kw in enumerate(keywords):
conditions.append(
f"(a.summary LIKE :kw{i} OR a.app_name LIKE :kw{i} OR a.entities LIKE :kw{i})"
)
# AND 组合所有关键词
where_clause = " AND ".join(conditions)

FTS 索引自动维护

系统会自动检测 FTS 索引是否过期(行数少于已完成的分析数量),并在查询时自动重建:

fts_count = db.execute(text("SELECT count(*) FROM analysis_fts")).scalar()
analysis_count = db.execute(text("SELECT count(*) FROM analyses WHERE status = 'done'")).scalar()
if fts_count < analysis_count:
_build_fts_index(db)

Web Search 实现

Web Search 支持两个搜索引擎,按优先级使用:

搜索引擎API 端点超时返回字段
Tavilyhttps://api.tavily.com/search15stitle, url, content
Bravehttps://api.search.brave.com/res/v1/web/search15stitle, url, description

记忆注入

Agent 在第一轮对话时会自动加载用户记忆作为上下文:

# services/agent.py - chat()
if round_num == 0:
from services.memory import get_memories_as_context
memories_ctx = get_memories_as_context(db, conv.device_id or "", limit=8)
if memories_ctx:
system_content += f"\n\n{memories_ctx}"

记忆以 ## 用户记忆 标题注入 System Prompt,格式如下:

## 用户记忆
- [long_term] [people] 用户的同事叫张三
- [long_term] [finance] 用户持有 NVDA 股票
- [short_term] [schedule] 下周三有项目评审会议

文件附件

支持多模态对话,用户可以发送图片或其他文件作为附件:

# api/chat.py - send_message_with_file()
if file and file.filename:
file_bytes = await file.read()
if len(file_bytes) > 20 * 1024 * 1024: # 20MB 限制
raise HTTPException(status_code=413, detail="File too large")
file_info = {
"filename": file.filename,
"mime": file.content_type,
"size": len(file_bytes),
"base64": base64.b64encode(file_bytes).decode(),
}

图片附件会被转换为多模态消息格式:

if mime.startswith("image/"):
user_content = [
{"type": "text", "text": user_message or "请分析这张图片"},
{"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}},
]

非图片文件则以文本形式附加:

else:
user_content = f"{user_message}\n\n[附件: {file_info['filename']} ({file_info['size']} bytes)]"

技能系统

技能 (Skill) 允许为 Agent 注入额外的 System Prompt 指令,实现特定任务的定制化:

内置技能

ID名称描述
summarize总结截图分析最近截图,生成摘要笔记
reminders提取提醒从截图中提取所有提醒事项
research深度研究对截图主题进行深入研究
stock股票分析整理截图中的股票和金融信息

技能注入方式

# services/agent.py
system_content = SYSTEM_PROMPT
if skill_prompt and round_num == 0:
system_content += f"\n\n## 当前技能指令\n{skill_prompt}"

技能的 System Prompt 仅在第一轮对话中注入。

错误处理

LLM 配置检查

对话前先检查 LLM 是否已配置:

config_check = check_llm_config()
if not config_check["ok"]:
return {
"content": f"警告: {config_check['error']}",
"error_type": "llm_not_configured",
}

LLM 调用错误

LLM 调用失败时返回用户友好的错误消息:

except Exception as e:
error_msg = format_llm_error(e)
return {
"content": f"警告: {error_msg}",
"error_type": "llm_error",
}

Android 端错误映射

Android 端将网络错误映射为用户友好的中文提示:

// ApiClient.kt
is java.net.SocketTimeoutException -> ChatResult(
errorMessage = "请求超时,AI 可能正在处理大量内容"
)
is java.net.ConnectException -> ChatResult(
errorMessage = "无法连接服务端: ${lastException?.message}"
)

超时保护

Agent 循环最多执行 3 轮,超时后返回友好提示:

for round_num in range(settings.agent_max_rounds): # 默认 3
...
return {"content": "抱歉,处理超时,请重试。", "tool_calls": []}

对话管理

会话管理

操作API说明
创建/继续对话POST /api/chat/sendconversation_id 可选,自动生成 16 位 hex
带附件对话POST /api/chat/send-with-file支持图片和其他文件
列出会话GET /api/chat/conversations按更新时间倒序,含消息数和最后消息预览
获取会话详情GET /api/chat/conversations/{id}包含完整消息历史
获取增量消息GET /api/chat/conversations/{id}/messages?after_id=N仅返回指定 ID 之后的消息
删除会话DELETE /api/chat/conversations/{id}级联删除所有消息

conversation_id 格式

会话 ID 为 1-64 位小写十六进制字符串:

_CONV_ID_RE = re.compile(r'^[a-f0-9]{1,64}$')

历史消息限制

Agent 构建对话历史时,最多取最近 20 条消息:

limit = settings.agent_history_limit # 默认 20
messages = db.query(ChatMessage)
.filter(ChatMessage.conversation_id == conversation_id)
.order_by(ChatMessage.created_at.desc())
.limit(limit)
.all()
messages.reverse() # 恢复时间顺序

后台记忆提取

每轮对话结束后,系统会在后台异步提取记忆,使用独立的数据库会话:

# 使用信号量限制并发数为 2
_memory_semaphore = asyncio.Semaphore(2)

async def _extract_memories_async(text, conversation_id, device_id):
async with _memory_semaphore:
db = SessionLocal()
try:
await extract_memories_from_text(text, "chat", conversation_id, device_id, db)
finally:
db.close()

System Prompt

Agent 的 System Prompt 定义了其行为准则:

你是 Evatar,一个智能个人助手。你拥有用户的手机截图知识库。

## 工具使用策略
### search_knowledge — 用核心关键词搜索
### search_multi — 一次传入多个相关关键词(推荐)
### get_recent — 获取最近截图(无需搜索词)
### web_search — 搜索互联网

## 搜索策略
1. 用户问题具体 -> search_knowledge 用核心词
2. 用户问题宽泛 -> search_multi 用多个相关词
3. 用户问"最近" -> get_recent 先看近期内容
4. 第一次搜不到 -> 换词重试
5. 找到部分信息 -> 告诉用户找到了什么

## 回答风格
- 用中文回答,简洁有条理
- 用 Markdown 表格整理结构化数据
- 引用截图中的具体信息