QQ Bot KIM: Building a Personified LLM Agent from Scratch
NapCatQQ + OneBot V11 + Agent Loop + Multi-engine search + 6-layer security + 3-tier memory — the complete technical record of turning a QQ bot into a LLM Agent with memory and personality.
The Situation That Drove Me Here
I'm in a dev group chat that has a bot. Every time someone asks a question, it either stays silent or drops a completely nonsensical reply — like it scraped a paragraph from some SEO spam site and stitched it together.
Even worse: it has zero memory. You tell it "stop spamming" and one second later it fires off three more messages. No concept of context whatsoever.
My thought was simple: a group-chat bot should at least act like a person.
Not that "Hello, I am an intelligent assistant, how may I help you" uncanny-valley nonsense. A real person who hangs out in the chat, speaks when there's something to say, shuts up when there isn't, and actually remembers what was said five minutes ago.
So I built KIM.
Why KIM
The name comes from Kim Kitsuragi — from Disco Elysium.
If you've played the game you know exactly what I mean. Kim isn't a motormouth. He doesn't farm presence, never steals your spotlight. But he's always there with a precise judgement the moment you need one. He observes, stays quiet, and says the right thing at the right time.
That's KIM's personality design.
Not the "hey everyone I'm your friendly AI assistant" greasiness. More like: someone in the group asks a question, KIM happens to know the answer, and calmly chimes in. If nobody responds, it doesn't care. It remembers what you said last round and carries the conversation forward naturally.
Personality isn't decoration bolted onto a system. Personality is a product of system architecture. More on that later.
Tech Stack: Why NapCatQQ + OneBot
Not go-cqhttp
Honestly, go-cqhttp was my first instinct. Mature ecosystem, tons of documentation, active community. But it has a fatal flaw — it's based on simulated QQ protocol login, and Tencent has been cracking down harder and harder on protocol bots. go-cqhttp stopped maintenance at the end of 2023. Building on an abandoned project means one protocol change away from total collapse.
Not Mirai
Mirai is written in Kotlin, works at a lower protocol level, and is genuinely more flexible. But this project is a lightweight group-chat bot, not an excuse to layer another HTTP adapter on top of Mirai. The Kotlin ecosystem is extra baggage I don't need — I don't want to maintain a JVM process just to run a chat bot.
Chose NapCatQQ
NapCatQQ takes a different approach: instead of protocol simulation, it's a reverse-engineered decorator on top of the official QQ client. You install QQ 3.2.21 (NT architecture), NapCat injects itself as a plugin, and exposes standard OneBot V11 WebSocket + HTTP interfaces.
The advantages are clear:
- Protocol layer follows official updates — QQ upgrades, the NapCat community tracks it, your bot code doesn't move.
- OneBot V11 is a standard interface — stable API contract, mature community toolchain. Swap backends by changing the adapter; business code stays put.
- Simple deployment — one npm package + one config file.
The tradeoff: you're running a full QQ client process, which is noticeably heavier than go-cqhttp. For me — ~22MB RAM — it's acceptable.
NapCatQQ → WS 6701 → bot.py (750 lines)
Architecture: 750 Lines of Agent Loop
The entire core bot is 750 lines of Python. Not showing off — it genuinely doesn't need more.
Agent Loop
The core logic is an agent_loop with minimal configuration:
MAX_TURNS=5, MAX_TOOLS=2
What does this mean? Every time a user message arrives, the agent can think for at most 5 rounds (think → tool call → observe result → think again), with at most 2 tool calls per round. Beyond that limit, it wraps up and replies — preventing the bot from infinitely recursing on any single question.
This limit is critical in practice. An unbounded agent loop is a ticking time bomb. We've seen the bot search for a weather forecast, get hooked, and chain 10 API calls in a row.
Multi-Engine Search
KIM connects to three search/tool categories:
- DuckDuckGo: general search, no API key required
- get_weather: weather queries, lightweight and practical
- get_stock: stock lookups — highly requested by group members
Technically nothing fancy here — each tool is an async function registered in the agent's toolbox, and the loop dispatches them.
But there's one tradeoff worth discussing: why DuckDuckGo instead of a "stronger" search engine?
The answer is pragmatic: zero API key overhead. I maintain this bot myself, and I don't want to add a SerpAPI bill. DDG is admittedly worse than Baidu for Chinese results, but for group-chat scenarios — Q&A, explaining concepts, checking weather — it's good enough. Good enough beats perfect every time.
Conversation Continuity
The hardest technical problem for a group-chat bot isn't "answering" — it's "when to chime in."
KIM's trigger system is layered:
- @KIM 100% trigger — if you're mentioned, you reply.
- Keyword match 3x coverage weight — when the chat mentions code, AI, or specific topics, KIM jumps in.
- Tiered probability trigger: HIGH 50%, MED 35%, LOW 20%, base 30%.
- Adaptive backoff: repeated triggers in a short window reduce probability — prevents the bot from interrupting people.
Continuity is even more carefully designed:
- 2-minute window after bot replies: if the same person speaks again, context auto-continues.
- Cross-person continuation with a 30-second window: A asks, B follows up — bot understands this is the same topic.
- QQ reply chains map directly to context continuation.
All of this is roughly 150 lines of code. But it's what makes KIM feel "intelligent." A bot that can't understand conversational continuity is an amnesiac — every message is a fresh start.
Memory: The Real Differentiator
Memory is the watershed moment for LLM agents.
A bot without memory is a goldfish. Every message is the first time it's ever seen you.
KIM uses three-tier memory:
-
Token budget trimming: context windows aren't infinite. Before each conversation turn, the system auto-calculates the current context token count and trims early messages if it exceeds the budget. This is the survival layer — without it, long conversations blow through the context window.
-
Deque short-term buffer: the last N messages stay in memory via Python's deque. Group chats generate high message volume — you can't write everything to long-term storage. The short-term buffer is a staging layer; only genuinely important content gets persisted.
-
LLM consolidation to long-term: every hour, the system hands the deque's key information to the LLM itself for condensation — using a
max_tokens=4000budget, it compresses the past hour into a few core memories and writes them to SQLite.
SQLite WAL mode. Zero external databases. One file handles all persistence.
Frankly, this design isn't optimal — a production system would use a vector database for semantic retrieval. But my constraint is zero external dependencies. Something you can deploy with a single pip install — isn't that better than a microservice architecture?
Design decisions serve constraints, not perfection.
6-Layer Security
A group-chat bot exposed to the public internet — security isn't optional, it's the floor.
KIM has 6 layers of defense, outside-in:
| Layer | Measure |
|---|---|
| Network | ufw firewall, 127.0.0.1 only |
| Process | systemd sandbox (ProtectSystem, NoNewPrivileges) |
| Auth | OneBot access token |
| Input | Injection detection (safety.py) |
| Tool | Search sanitization + 10s timeout |
| Output | Strip markdown, plain text only |
Network & Process Layers
These are infrastructure-level. ufw config is 5 lines, systemd config is 10 lines, but they're the first line of defense. Without these two layers, even the strongest application-level security is pointless.
KIM runs under systemd with ProtectSystem=strict and NoNewPrivileges=yes, locking down process privileges. Even if the bot is compromised, the attacker can't escalate to system level.
Input Layer
injection_detection.py is a dedicated module. Group chats are open environments — anyone can drop a message that carries a prompt injection. KIM filters all input, not with simple keyword matching, but with semantic pattern detection.
Honestly, this module isn't mature yet. Prompt injection is an ongoing arms race, not something you solve with one function and call it done.
Tool Layer
Every tool call has a 10-second timeout. Search API responses are sanitized — potentially malicious content is stripped out. The design philosophy: better for the bot to say "I don't know" than to say one wrong thing.
Output Layer
Strip markdown. Plain text only. QQ's rendering engine has poor markdown support, and markdown injection is itself an attack surface. Plain text is both safe and universally compatible.
Pitfall Catalog
This section is probably the most valuable part of this post. Most LLM-and-bot articles tell you "how to build it" but never "how it blows up."
Package Name Hijacking: duckduckgo_search v9.14.4 is Fake
(Feel the anger in this one.)
Installed duckduckgo_search via pip, spent a whole day debugging errors. Turns out v9.14.4 is a malicious squatted metasearch package that has nothing to do with duckduckgo. The correct package is duckduckgo_search>=8.1.
This was a brutal lesson: Python package names aren't identifiers — they're trust credentials. Name squatting on PyPI is common. Checking the version number and release date before installing is basic self-defense.
WebSocket Connected but No Messages
NapCatQQ's WebSocket showed as connected, but the bot never received any group message events.
After a night of debugging, the root cause: missing ONEBOT_ACCESS_TOKEN config. NapCat's WebSocket interface silently refuses when the access token is misconfigured — the TCP handshake succeeds, but no events are ever pushed.
Silent failure is the nastiest bug category. No error logs, no 4xx/5xx, nothing. Just quietly doing nothing.
QQ 3.2.21 Dropped JSON Cards
Wrote code using build_link_card to generate nice link cards. After deployment, every card rendered as blank.
The QQ NT architecture (3.2.21) removed custom JSON card capability. A feature that worked on older QQ was simply deprecated in the new version.
Solution: ripped it all out, went back to plain text + links.
Provider API Timeout
The LLM provider API had a high timeout rate in group-chat scenarios. The reason is straightforward — LLM inference isn't instantaneous, and multiple requests queue up during peak chat hours.
The default 25-second timeout was too tight. Bumped it to 60 seconds.
Added another layer: if the LLM times out, the bot replies with "System busy, try again later" instead of hanging there waiting.
Deployment: systemd + Done
Production deployment is exactly two files: one systemd unit file, one ufw config.
[Service]
ExecStart=/usr/bin/python3 /home/bot/kim/bot.py
Restart=always
RestartSec=10
ProtectSystem=strict
NoNewPrivileges=yes
Memory footprint: ~22MB. Zero external databases. One cron job restarts it at 4 AM daily — not to fix a memory leak, just defense against "stuff that goes wrong when you leave it running too long."
NapCatQQ is configured with NAPCAT_QUICK_PASSWORD_MD5 for automatic password login. After a bot restart, QQ comes back online automatically — no human intervention needed.
The Real Insight: Giving You ChatGPT's API Doesn't Mean You Can Build a Good Bot
This is the most important thing I want to say.
The tech stack doesn't matter. Agent loops, memory systems, security defenses — those are all copyable homework. The real dividing line is: have you thought through what your bot is, in what scenario, with what identity, and how it should exist?
A lot of people approach bot-building with "I have a powerful LLM, so I'll make a bot that answers every question." That's capability-driven thinking, not scenario-driven thinking.
KIM's capability isn't "answering everything." KIM's capability is "being a useful person in a group chat."
Those two sentences are not the same.
When you shift from "answering questions" to "being a useful person," a lot of design decisions become obvious: knowing when to speak and when to shut up matters more than how accurate your answers are. Security matters more than features — a bot that says the wrong thing is more harmful than a bot that says nothing. A bot with memory isn't a feature enhancement — it's the foundation of identity.
At the end of the day, you're not building a bot. You're building a digital persona.
And persona was never a technical problem.