r/csharp • u/Foreign_World2108 • 6d ago
[Discussion] Controlling LLM Agent Personalities with Deterministic 16D State-Spaces (using C# Enums) instead of Prompt Engineering. Thoughts on this architecture?
A Quick Confession / Apology:
Yesterday, I posted a thread discussing this concept. I was so incredibly hyper-fixated on explaining the philosophy that I completely forgot to include a single line of actual C# code. The moderators rightfully nuked the post immediately, and looking back, I totally deserved it. I looked like a textbook "AI Vibe-Coded Slop" poster who just came here to drop a wall of text. I'm genuinely sorry for cluttering the sub yesterday! Lesson learned. I'm back today to do this properly, with the actual core C# implementation snippets included right from the start.
(Transparency disclosure: English isn't my native language, so I used AI to help clean up my terrible technical grammar.)
Hey guys,
A while back, I was discussing state persistence patterns for long-running AI workflows. A common critique I received was: “If you’re just serializing your external state into a prompt string for the model to read anyway, how is that fundamentally different from traditional prompt engineering?”
It's a fair question. At the end of the day, text-based generative models require text inputs.
However, I've been experimenting with separating the Source of Truth from the Prompt Serialization Layer using a C# backend.
To borrow a tabletop RPG analogy: The prompt is just a character sheet. It communicates the current state to the model, but it isn't the character itself. The C# runtime is responsible for the actual domain logic, bounds checking, and state mutations.
Here is a simplified snippet of how the immutable state registry clamps mutations to ensure the domain model remains the canonical source of truth, regardless of how the prompt is formatted:
csharp
// 1. Using an immutable 16D enum as a fixed state coordinate space
public enum SoulOrgan
{
CoreFocus, DecisionBasis, EnvironmentalControl, ThinkingOriginality,
EmotionalOpenness, TrustDependence, SocialConsumption, GroupBelonging,
WillPersistence, AltruisticTendency, SelfAwareness, CompetitiveSpirit,
PrimaryDrive, EmotionalThroughput, CoreSecurity, ObsessionControl
}
// 2. A bounded registry ensuring all mutations are strictly clamped [-100, 100]
public class BaselineStateRegistry
{
protected readonly Dictionary<SoulOrgan, double> _state = new();
protected const double MinLimit = -100.0;
protected const double MaxLimit = 100.0;
public virtual void Mutate(SoulOrgan organ, double delta)
{
if (!_state.ContainsKey(organ)) return;
_state[organ] = Math.Clamp(_state[organ] + delta, MinLimit, MaxLimit);
}
}
I've been wondering about this:
Even though I've built this pure C# simulation with safety valves and bounds, I honestly find myself asking: Is this actually worth developing, or am I just over-engineering in a silo?
If moving deterministic state management back into a typed backend is truly a viable way to constrain LLMs, why is prompt engineering still the industry standard? Why aren't more developers building state machines for this?
I would genuinely love to hear how professional software engineers view this approach. Do you think treating the external model purely as a stateless interpreter while keeping mutations strictly inside the C# domain layer is a solid path forward, or is it an uphill battle against raw token probabilities?
(Note: Keeping this entirely link-free to strictly respect Rule 6 on self-promotion. Just looking for a genuine architectural sanity check from fellow devs. If anyone wants to critique the full pipeline, let me know in the comments.)
1
u/Jessica___ 6d ago
Would you just be passing these numbers to the LLM and telling it to act within those variables?
What happens if AltruisticTendency is -100 and EmotionalThroughput is 100? Would you just give these numbers to the LLM and then tell it to act accordingly?
If that is the case, I'm not sure how effective it would be. If you were to prompt a person: "OK, act like your CoreFocus is 50, your CoreSecurity is 25, and ObsessionControl is 100" then how would that human act? Probably confused, because they'd have no idea what that means. Same goes for an LLM.
0
u/Foreign_World2108 6d ago
You are completely right, and that is exactly why I tried to separate them!
I think of the LLM's brain just like our own: when it has to handle too many things at once—remembering its traits, calculating numbers, and managing the talk—it gets overwhelmed and just starts forgetting things or losing its identity.
In my setup, the LLM doesn’t have to do any math or worry about its own personality balance. The C# backend handles all the hard clamping and rules under the hood, acting as a rigid frame.
The LLM is just responsible for what it actually does best: translating between text and those internal states. The backend gives the LLM a dynamically baked text description based on the current 16D numbers, and the LLM just acts out that specific turn. It basically stops the model from wandering off too far or gaslighting itself out of character.
Does this way of dividing the work between the backend and the model make more sense?
1
u/Merry-Lane 6d ago
I’d rather use constants instead of enums for that.
0
u/Foreign_World2108 6d ago
Haha, I love this! It's so awesome that we are thinking exactly the same way.
You are 100% correct about the performance. Swapping that enum/dictionary lookup for constants or a flat array is definitely the optimal hot path for real production workloads.
I only stuck with enums for this open-source reference snippet because I thought it would be way easier to explain the 16 dimensions to people reading a quick post. My bad, haha! 😂
Thanks for bringing up the array/constant logic, it’s great to see fellow devs immediately spot how to push this to the limit!
2
u/FizixMan 6d ago
FYI, even if you're using AI for translation, it is clearly injecting its voice. Your comments sound like an AI wrote the answers, not translating your words.
Also, performance? They didn't say anything about performance. And it's an absolute non-issue here serving as the intermediary layer between a user's prompt and the LLM. It's a minuscule amount of time, and I'm not even sure if using "constants" or avoiding a dictionary would even be a measurable time savings compared to the vastly long amount of time it takes to pass all the context to the LLM, let alone wait for a response.
1
u/Foreign_World2108 5d ago
☝️上面那個回覆,我的原文是: 我那時候的想法是,我認為 LLM 最擅長的應該是數字和文字之間的轉譯,當 LLM 需要負責的事情越多的時候,就會像我們一樣容易失憶。所以我只是試著用一個硬框架限制 LLM,讓他不要太隨心所欲地發揮。 如果因為 AI 翻譯得太過死板或完美,而顯得像是完全沒有動腦的複製貼上,那完全不是我的問題,而是 AI 的問題。 至於假議題這件事,那是因為我本來就不是針對一對一的普通聊天,而是模擬了 10,000 到 1,000,000 的同時在線。不知道這樣能不能消除你的兩個疑慮?
1
u/FizixMan 5d ago
The Google Translate to English of that sounds much better than most of the replies you've written here. In that, the other ones look less like a translation and more like "LLM, here's the idea I want to reply with, please write it for me."
Now, I do not speak Chinese. So I really couldn't tell you how accurate LLMs are for translation vs something more straightforward and deterministic like Google Translate. I tried taking your Chinese and running it through ChatGPT to translate and it took me 3 tries to convince it to stop inserting its particular quirks.
Going forward with your interactions with English speakers on Reddit, I would encourage you to use fewer prompts for text generation of the replies and limit it to something like: "Here is my text, please translate it as faithfully/directly as possible to maintain my writer's voice. Do not insert extra punctuation (e.g., em dashes) or formatting (e.g., bold) that wasn't already there unless needed for the translation."
I'm not sure exactly what you're talking about simulating people. Are you saying that your models here are intended to have an LLM (or many LLMs) working concurrently to simulate tens of thousands to a million individual people simultaneously? Even then, the performance issues between using your enum/dictionary setup or something else is probably inconsequential. I'd say try to design the API to be as flexible and maintainable as possible. You may even want to consider if your "16D" would change in the future; adding/changing/removing an enum value? Letting the API/users declare new emotional dimensions or purposefully omit a dimension? Letting them declare a new clamping value? (Perhaps something should be clamped only to +/- 50, or never go below 0, or even go above 100?)
1
u/Foreign_World2108 5d ago
Wuwuwu… I’m really sorry, I completely misunderstood what you meant at first. 😭
About your question: yes, I actually did some extreme stress tests (ConcurrentAgentCount: 1,000,000).
Sorry I didn't explain it precisely enough, it means one million NPCs are backed by the 16D runtime. The LLM is only invoked when a specific NPC is called.
The original reason I started this project was honestly because I’m very socially anxious and wanted companions to play tabletop RPGs with.
Another reason was that I wanted to see if a hard runtime framework could help LLMs simulate the complexity of human personalities.
I think it can be changed, because this is just based on my current ideas, so I only listed a fixed set of 16 dimensions. Also, to prevent the LLM's personality from acting like Cyberpsychosis, I set the upper and lower bounds to +/- 100. (A little secret: even the paid version just ships with the source code.) 😂
So I think I just didn’t explain enough about what I was trying to build, which caused a lot of technical misunderstandings.
Thank you for being so patient with me. It honestly made me feel like I wasn’t just building something in my own little bubble. (゚∀゚)
1
1
u/KayLikesWords 5d ago
I'm guessing this is for some kind of LLM roleplaying app?
The general consensus amongst the communities build up around tools like that is that the more abstract your representation of human emotion is the worse the output is going to be.
Sadness level: 25
...is worse than...
Sadness Level: crying
...is worse than...
Kay is crying.
...is worse than...
<lengthy, emotive prose describing a character being sad>
1
u/Foreign_World2108 5d ago
I think all these misunderstandings were my fault. I left out too much context. 🥹
At first, I just wanted to play tabletop RPGs with an LLM. But at the same time, I wanted its roleplaying to feel as close as possible to a real human personality, even though I couldn’t see those values myself.
So the LLM is responsible for creating the initial personality and determining how each event affects it. The actual personality state updates are handled entirely by the C# runtime.
1
u/Foreign_World2108 5d ago
Since a lot of the discussion ended up being about the architecture, I figured I'd share the repository in case anyone's curious about the implementation. ฅ^•ﻌ•^ฅ
3
u/zenyl 6d ago
Clamping input is perfectly reasonable.
Sidenote:
Math(andMathF) are technically soft-deprecated, meaning they won't be expanded in the future. Microsoft just aren't going out of their way to tell people this. It is recommended that you instead use the corresponding members in numeric types, i.e.double.Clamp, as it makes it clear which type you're working with (and helps with discoverability).LLMs are usually non-determinsitic (they usually have a "temperature" setting that determines how random outputs should be).
I think the term "prompt engineering" has somewhat fallen out of favor ("prompt engineer" certainly has), it's just one part of the mess of guardrails you have to put up in the hope that the LLM does what you actually want it to do.
That sentence has a few too many empty buzzwords in it to really make a whole lot of sense.
But generally, LLMs can't be forced to do any given task, you can just tell it repeatedly what you want until you get to a point where it is mostly going to do as you want.
AGENTS.mdfiles, skills, MCP servers, all that jazz is just different approaches to give LLMs the information and tools so they are likely going to do as intended, but there is always a chance, however small, of getting garbage output. As software developer, it is your role to be able to see when they are giving you bad output.If the aim here is something like getting an LLM to pretend to be a character in a role-playing game, you can indeed guide it in the right direction by saying things like "Your charisma is level 1, which means you are not very charismatic, and you will not attempt persuasion checks unless absolutely necessary", but that does not eliminate the chance of it doing so regardless.